improve cold boot
This commit is contained in:
parent
e89f8d76fb
commit
6dfe2b5c81
6 changed files with 462 additions and 70 deletions
|
|
@ -7,8 +7,39 @@
|
||||||
<link href="app.css" type="text/css" rel="stylesheet"/>
|
<link href="app.css" type="text/css" rel="stylesheet"/>
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
<link href="assets/overrides.css" type="text/css" rel="stylesheet"/>
|
<link href="assets/overrides.css" type="text/css" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
#ignis-status {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
background: #202020;
|
||||||
|
color: #b3b3b3;
|
||||||
|
font: 14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
z-index: 9999;
|
||||||
|
transition: opacity 200ms ease-out;
|
||||||
|
}
|
||||||
|
#ignis-status.fade { opacity: 0; pointer-events: none; }
|
||||||
|
#ignis-status img {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
animation: ignis-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
#ignis-status-label { font-size: 13px; opacity: 0.75; }
|
||||||
|
@keyframes ignis-pulse {
|
||||||
|
0%, 100% { opacity: 0.85; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.04); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-dark">
|
<body class="theme-dark">
|
||||||
|
<div id="ignis-status">
|
||||||
|
<img src="favicon.png" alt=""/>
|
||||||
|
<div id="ignis-status-label">Loading Obsidian...</div>
|
||||||
|
</div>
|
||||||
<!-- Ignis shims: must run before any Obsidian code. -->
|
<!-- Ignis shims: must run before any Obsidian code. -->
|
||||||
<script type="text/javascript" src="__IGNIS_UI_SRC__"></script>
|
<script type="text/javascript" src="__IGNIS_UI_SRC__"></script>
|
||||||
<script type="text/javascript" src="__SHIM_LOADER_SRC__"></script>
|
<script type="text/javascript" src="__SHIM_LOADER_SRC__"></script>
|
||||||
|
|
@ -16,11 +47,41 @@
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var scripts = __OBSIDIAN_SCRIPTS__;
|
var scripts = __OBSIDIAN_SCRIPTS__;
|
||||||
|
var label = document.getElementById("ignis-status-label");
|
||||||
|
var status = document.getElementById("ignis-status");
|
||||||
|
var loaded = 0;
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (label) {
|
||||||
|
label.textContent = "Loading Obsidian " + loaded + "/" + scripts.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
if (!status) return;
|
||||||
|
status.classList.add("fade");
|
||||||
|
setTimeout(function () {
|
||||||
|
if (status && status.parentNode) status.parentNode.removeChild(status);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
for (var i = 0; i < scripts.length; i++) {
|
for (var i = 0; i < scripts.length; i++) {
|
||||||
var s = document.createElement("script");
|
var s = document.createElement("script");
|
||||||
s.type = "text/javascript";
|
s.type = "text/javascript";
|
||||||
s.src = scripts[i];
|
s.src = scripts[i];
|
||||||
s.async = false;
|
s.async = false;
|
||||||
|
s.onload = function () {
|
||||||
|
loaded++;
|
||||||
|
update();
|
||||||
|
if (loaded === scripts.length) done();
|
||||||
|
};
|
||||||
|
s.onerror = function () {
|
||||||
|
loaded++;
|
||||||
|
update();
|
||||||
|
if (loaded === scripts.length) done();
|
||||||
|
};
|
||||||
document.body.appendChild(s);
|
document.body.appendChild(s);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ const fsRoutes = require("./routes/fs");
|
||||||
const vaultRoutes = require("./routes/vault");
|
const vaultRoutes = require("./routes/vault");
|
||||||
const proxyRoutes = require("./routes/proxy");
|
const proxyRoutes = require("./routes/proxy");
|
||||||
const versionRoutes = require("./routes/version");
|
const versionRoutes = require("./routes/version");
|
||||||
|
const bootstrapRoutes = require("./routes/bootstrap");
|
||||||
|
|
||||||
app.use("/assets", express.static(path.join(__dirname, "assets")));
|
app.use("/assets", express.static(path.join(__dirname, "assets")));
|
||||||
|
|
||||||
|
|
@ -60,6 +61,7 @@ app.use("/api/vault", vaultRoutes);
|
||||||
app.use("/api/proxy", proxyRoutes);
|
app.use("/api/proxy", proxyRoutes);
|
||||||
app.use("/api/version", versionRoutes);
|
app.use("/api/version", versionRoutes);
|
||||||
app.use("/api/plugins", pluginRoutes);
|
app.use("/api/plugins", pluginRoutes);
|
||||||
|
app.use("/api/bootstrap", bootstrapRoutes);
|
||||||
|
|
||||||
// Serve vault files for resource URLs (images, attachments, etc.)
|
// Serve vault files for resource URLs (images, attachments, etc.)
|
||||||
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
|
||||||
|
|
@ -153,6 +155,9 @@ const server = app.listen(config.port, async () => {
|
||||||
|
|
||||||
await updateBridgePluginInAllVaults(config.vaultRoot);
|
await updateBridgePluginInAllVaults(config.vaultRoot);
|
||||||
await initPlugins({ app, config, wss, watcher });
|
await initPlugins({ app, config, wss, watcher });
|
||||||
|
bootstrapRoutes.warmUp().catch((e) =>
|
||||||
|
console.warn("[bootstrap] warm-up error:", e.message),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const wss = setupWebSocket(server);
|
const wss = setupWebSocket(server);
|
||||||
|
|
|
||||||
252
server/routes/bootstrap.js
vendored
Normal file
252
server/routes/bootstrap.js
vendored
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
// Bootstrap endpoint for cold start.
|
||||||
|
//
|
||||||
|
// Combines vault info, vault list, metadata tree, and plugin list into a
|
||||||
|
// single pre-compressed response. Cache is per-vault and invalidated by
|
||||||
|
// directory mtime check + explicit invalidateVault() calls from the write/delete routes.
|
||||||
|
|
||||||
|
const express = require("express");
|
||||||
|
const fs = require("fs");
|
||||||
|
const fsp = fs.promises;
|
||||||
|
const path = require("path");
|
||||||
|
const zlib = require("zlib");
|
||||||
|
const config = require("../config");
|
||||||
|
const { isBridgePluginInstalled, getIgnisMeta } = require("../bridge-plugin");
|
||||||
|
const { getDiscoveredPlugins } = require("../plugin-system/manager");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// vaultId -> { response, dirMtimes, compressed: { br, gz } }
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
// vaultId -> Promise<entry> (in-flight build dedup)
|
||||||
|
const pendingBuilds = new Map();
|
||||||
|
|
||||||
|
function preCompress(buf) {
|
||||||
|
return Promise.all([
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
zlib.brotliCompress(
|
||||||
|
buf,
|
||||||
|
{ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } },
|
||||||
|
(err, result) => (err ? reject(err) : resolve(result)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
zlib.gzip(buf, { level: 6 }, (err, result) =>
|
||||||
|
err ? reject(err) : resolve(result),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]).then(([br, gz]) => ({ br, gz }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function walkTree(rootPath) {
|
||||||
|
const tree = {};
|
||||||
|
const dirMtimes = {};
|
||||||
|
|
||||||
|
async function walk(dir, prefix) {
|
||||||
|
const stat = await fsp.stat(dir);
|
||||||
|
dirMtimes[prefix] = stat.mtimeMs;
|
||||||
|
|
||||||
|
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const rel = prefix ? prefix + "/" + entry.name : entry.name;
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
tree[rel] = { type: "directory" };
|
||||||
|
await walk(full, rel);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const s = await fsp.stat(full);
|
||||||
|
|
||||||
|
tree[rel] = {
|
||||||
|
type: "file",
|
||||||
|
size: s.size,
|
||||||
|
mtime: s.mtimeMs,
|
||||||
|
ctime: s.ctimeMs,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
tree[rel] = { type: "file" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walk(rootPath, "");
|
||||||
|
|
||||||
|
return { tree, dirMtimes };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildVaultInfo(vaultId, vaultPath) {
|
||||||
|
const pluginInstalled = await isBridgePluginInstalled(vaultPath);
|
||||||
|
const ignisMeta = await getIgnisMeta(vaultPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: vaultId,
|
||||||
|
name: vaultId,
|
||||||
|
path: vaultPath,
|
||||||
|
platform: process.platform,
|
||||||
|
version: config.obsidianVersion,
|
||||||
|
ignisPlugin: {
|
||||||
|
installed: pluginInstalled,
|
||||||
|
prompted: ignisMeta.pluginPrompted || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVaultList() {
|
||||||
|
return Object.entries(config.vaults).map(([id, vaultPath]) => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
path: vaultPath,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dirMtimesUnchanged(vaultPath, dirMtimes) {
|
||||||
|
const checks = await Promise.all(
|
||||||
|
Object.entries(dirMtimes).map(async ([relDir, oldMtime]) => {
|
||||||
|
const absDir = relDir
|
||||||
|
? path.join(vaultPath, relDir.split("/").join(path.sep))
|
||||||
|
: vaultPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const s = await fsp.stat(absDir);
|
||||||
|
return s.mtimeMs === oldMtime;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return checks.every(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildEntry(vaultId) {
|
||||||
|
const vaultPath = config.getVaultPath(vaultId);
|
||||||
|
|
||||||
|
if (!vaultPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = cache.get(vaultId);
|
||||||
|
|
||||||
|
if (cached && (await dirMtimesUnchanged(vaultPath, cached.dirMtimes))) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = Date.now();
|
||||||
|
const [vault, { tree, dirMtimes }] = await Promise.all([
|
||||||
|
buildVaultInfo(vaultId, vaultPath),
|
||||||
|
walkTree(vaultPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
vault,
|
||||||
|
vaultList: buildVaultList(),
|
||||||
|
tree,
|
||||||
|
plugins: getDiscoveredPlugins(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonBuf = Buffer.from(JSON.stringify(response));
|
||||||
|
let compressed = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
compressed = await preCompress(jsonBuf);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[bootstrap] precompression failed:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = { response, dirMtimes, compressed };
|
||||||
|
cache.set(vaultId, entry);
|
||||||
|
|
||||||
|
const ms = Date.now() - t0;
|
||||||
|
const fileCount = Object.keys(tree).filter(
|
||||||
|
(k) => tree[k].type === "file",
|
||||||
|
).length;
|
||||||
|
const dirCount = Object.keys(dirMtimes).length;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[bootstrap] vault=${vaultId} build files=${fileCount} dirs=${dirCount} time=${ms}ms`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOrBuild(vaultId) {
|
||||||
|
if (pendingBuilds.has(vaultId)) {
|
||||||
|
return pendingBuilds.get(vaultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = buildEntry(vaultId).finally(() => {
|
||||||
|
pendingBuilds.delete(vaultId);
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingBuilds.set(vaultId, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateVault(vaultId) {
|
||||||
|
cache.delete(vaultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function warmUp() {
|
||||||
|
const ids = Object.keys(config.vaults);
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
try {
|
||||||
|
await buildEntry(id);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[bootstrap] warm-up failed for vault ${id}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const vaultId = req.query.vault || config.defaultVaultId;
|
||||||
|
|
||||||
|
if (!vaultId || !config.getVaultPath(vaultId)) {
|
||||||
|
return res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry = await getOrBuild(vaultId);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return res.status(404).json({ error: "Vault not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ae = req.headers["accept-encoding"] || "";
|
||||||
|
const { compressed } = entry;
|
||||||
|
let buf, encoding;
|
||||||
|
|
||||||
|
if (ae.includes("br") && compressed.br) {
|
||||||
|
buf = compressed.br;
|
||||||
|
encoding = "br";
|
||||||
|
} else if (
|
||||||
|
(ae.includes("gzip") || ae.includes("deflate")) &&
|
||||||
|
compressed.gz
|
||||||
|
) {
|
||||||
|
buf = compressed.gz;
|
||||||
|
encoding = "gzip";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf) {
|
||||||
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
|
res.setHeader("Content-Encoding", encoding);
|
||||||
|
res.setHeader("Content-Length", buf.length);
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
|
||||||
|
return res.status(200).end(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(entry.response);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[bootstrap] error:", e);
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
module.exports.invalidateVault = invalidateVault;
|
||||||
|
module.exports.warmUp = warmUp;
|
||||||
|
|
@ -4,6 +4,7 @@ const path = require("path");
|
||||||
const archiver = require("archiver");
|
const archiver = require("archiver");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
const { writeCoalesced, getPending } = require("../write-coalescer");
|
const { writeCoalesced, getPending } = require("../write-coalescer");
|
||||||
|
const bootstrapRoutes = require("./bootstrap");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -64,9 +65,17 @@ function getVaultRoot(req, res) {
|
||||||
res.status(404).json({ error: "Vault not found", id: vaultId });
|
res.status(404).json({ error: "Vault not found", id: vaultId });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req._vaultId = vaultId;
|
||||||
return vaultPath;
|
return vaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidateBootstrap(req) {
|
||||||
|
if (req._vaultId) {
|
||||||
|
bootstrapRoutes.invalidateVault(req._vaultId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve a client-provided path to an absolute path within a vault.
|
// Resolve a client-provided path to an absolute path within a vault.
|
||||||
// Strips leading slashes so paths from the client are always treated as relative to the vault root.
|
// Strips leading slashes so paths from the client are always treated as relative to the vault root.
|
||||||
function resolveVaultPath(vaultRoot, relativePath) {
|
function resolveVaultPath(vaultRoot, relativePath) {
|
||||||
|
|
@ -258,6 +267,7 @@ router.post("/writeFile", async (req, res) => {
|
||||||
|
|
||||||
const result = await writeCoalesced(resolved, data, encoding);
|
const result = await writeCoalesced(resolved, data, encoding);
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true, mtime: result.mtime, size: result.size });
|
res.json({ ok: true, mtime: result.mtime, size: result.size });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -275,6 +285,7 @@ router.post("/appendFile", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await fs.promises.appendFile(resolved, req.body.content, "utf-8");
|
await fs.promises.appendFile(resolved, req.body.content, "utf-8");
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -294,6 +305,7 @@ router.post("/mkdir", async (req, res) => {
|
||||||
recursive: !!req.body.recursive,
|
recursive: !!req.body.recursive,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -318,6 +330,7 @@ router.post("/rename", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await fs.promises.rename(oldResolved, newResolved);
|
await fs.promises.rename(oldResolved, newResolved);
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -342,6 +355,7 @@ router.post("/copyFile", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await fs.promises.copyFile(srcResolved, destResolved);
|
await fs.promises.copyFile(srcResolved, destResolved);
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -359,6 +373,7 @@ router.delete("/unlink", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await fs.promises.unlink(resolved);
|
await fs.promises.unlink(resolved);
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === "ENOENT") {
|
if (e.code === "ENOENT") {
|
||||||
|
|
@ -381,6 +396,7 @@ router.delete("/rmdir", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await fs.promises.rmdir(resolved);
|
await fs.promises.rmdir(resolved);
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -399,6 +415,8 @@ router.delete("/rm", async (req, res) => {
|
||||||
await fs.promises.rm(resolved, {
|
await fs.promises.rm(resolved, {
|
||||||
recursive: req.query.recursive === "true",
|
recursive: req.query.recursive === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
@ -453,6 +471,8 @@ router.post("/utimes", async (req, res) => {
|
||||||
req.body.atime / 1000,
|
req.body.atime / 1000,
|
||||||
req.body.mtime / 1000,
|
req.body.mtime / 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
invalidateBootstrap(req);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message, code: e.code });
|
res.status(500).json({ error: e.message, code: e.code });
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const {
|
||||||
setIgnisMeta,
|
setIgnisMeta,
|
||||||
installBridgePlugin,
|
installBridgePlugin,
|
||||||
} = require("../bridge-plugin");
|
} = require("../bridge-plugin");
|
||||||
|
const bootstrapRoutes = require("./bootstrap");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -68,6 +69,7 @@ router.post("/create", async (req, res) => {
|
||||||
await installBridgePlugin(vaultPath);
|
await installBridgePlugin(vaultPath);
|
||||||
|
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
|
bootstrapRoutes.invalidateVault(name);
|
||||||
|
|
||||||
res.json({ ok: true, id: name, path: vaultPath });
|
res.json({ ok: true, id: name, path: vaultPath });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -100,6 +102,8 @@ router.post("/rename", async (req, res) => {
|
||||||
await fs.promises.rename(vaultPath, newPath);
|
await fs.promises.rename(vaultPath, newPath);
|
||||||
|
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
|
bootstrapRoutes.invalidateVault(vaultId);
|
||||||
|
bootstrapRoutes.invalidateVault(newName);
|
||||||
|
|
||||||
res.json({ ok: true, id: newName, path: newPath });
|
res.json({ ok: true, id: newName, path: newPath });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -126,6 +130,7 @@ router.delete("/remove", async (req, res) => {
|
||||||
await fs.promises.rm(vaultPath, { recursive: true });
|
await fs.promises.rm(vaultPath, { recursive: true });
|
||||||
|
|
||||||
config.refreshVaults();
|
config.refreshVaults();
|
||||||
|
bootstrapRoutes.invalidateVault(vaultId);
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,34 @@ function resolveVaultId() {
|
||||||
window.__workspaceName = urlParams.get("workspace") || "";
|
window.__workspaceName = urlParams.get("workspace") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function initVaultConfig() {
|
// Single round-trip bootstrap: vault info + vault list + metadata tree + plugins.
|
||||||
try {
|
// Returns the parsed response, or null if the call failed (no vault, network error, etc.)
|
||||||
const vaultParam = window.__currentVaultId
|
function fetchBootstrap() {
|
||||||
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
if (!window.__currentVaultId) {
|
||||||
: "";
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
xhr.open(
|
||||||
|
"GET",
|
||||||
|
"/api/bootstrap?vault=" + encodeURIComponent(window.__currentVaultId),
|
||||||
|
false,
|
||||||
|
);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const info = JSON.parse(xhr.responseText);
|
return JSON.parse(xhr.responseText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ignis] Bootstrap fetch failed:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVaultInfo(info) {
|
||||||
window.__currentVaultId = info.id;
|
window.__currentVaultId = info.id;
|
||||||
localStorage.setItem("last-vault", info.id);
|
localStorage.setItem("last-vault", info.id);
|
||||||
window.__obsidianVersion = info.version || "0.0.0";
|
window.__obsidianVersion = info.version || "0.0.0";
|
||||||
|
|
@ -39,6 +53,33 @@ function initVaultConfig() {
|
||||||
|
|
||||||
console.log("[ignis] Vault:", window.__vaultConfig);
|
console.log("[ignis] Vault:", window.__vaultConfig);
|
||||||
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
|
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTree(tree) {
|
||||||
|
fsShim._metadataCache.populate(tree);
|
||||||
|
fsShim._metadataCache.set("", { type: "directory" });
|
||||||
|
fsShim._metadataCache.set("/", { type: "directory" });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[ignis] Metadata cache populated:",
|
||||||
|
fsShim._metadataCache.size,
|
||||||
|
"entries",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initVaultConfigFallback() {
|
||||||
|
try {
|
||||||
|
const vaultParam = window.__currentVaultId
|
||||||
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open("GET", "/api/vault/info" + vaultParam, false);
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
applyVaultInfo(JSON.parse(xhr.responseText));
|
||||||
} else {
|
} else {
|
||||||
console.warn("[ignis] No vault found, will show manager");
|
console.warn("[ignis] No vault found, will show manager");
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +88,7 @@ function initVaultConfig() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initVaultList() {
|
function initVaultListFallback() {
|
||||||
try {
|
try {
|
||||||
vaultService.listVaultsSync();
|
vaultService.listVaultsSync();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -55,7 +96,7 @@ function initVaultList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMetadataCache() {
|
function initMetadataCacheFallback() {
|
||||||
try {
|
try {
|
||||||
const vaultParam = window.__currentVaultId
|
const vaultParam = window.__currentVaultId
|
||||||
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
? "?vault=" + encodeURIComponent(window.__currentVaultId)
|
||||||
|
|
@ -67,17 +108,7 @@ function initMetadataCache() {
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const tree = JSON.parse(xhr.responseText);
|
applyTree(JSON.parse(xhr.responseText));
|
||||||
|
|
||||||
fsShim._metadataCache.populate(tree);
|
|
||||||
fsShim._metadataCache.set("", { type: "directory" });
|
|
||||||
fsShim._metadataCache.set("/", { type: "directory" });
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[ignis] Metadata cache populated:",
|
|
||||||
fsShim._metadataCache.size,
|
|
||||||
"entries",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("[ignis] Failed to fetch metadata tree:", xhr.status);
|
console.error("[ignis] Failed to fetch metadata tree:", xhr.status);
|
||||||
}
|
}
|
||||||
|
|
@ -114,24 +145,13 @@ function initPluginPrompt() {
|
||||||
// this prevents headless sync from being disabled as a result of a different device syncing "Active core plugins list".
|
// this prevents headless sync from being disabled as a result of a different device syncing "Active core plugins list".
|
||||||
// i.e ensure Ignis always has sync: false if headless sync is active.
|
// i.e ensure Ignis always has sync: false if headless sync is active.
|
||||||
// This may be somewhat overengineered. Could revisit later.
|
// This may be somewhat overengineered. Could revisit later.
|
||||||
function initCoreSyncGuard() {
|
function applyCoreSyncGuard(plugins) {
|
||||||
const vaultId = window.__currentVaultId;
|
const vaultId = window.__currentVaultId;
|
||||||
|
|
||||||
if (!vaultId) {
|
if (!vaultId || !plugins) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.open("GET", "/api/plugins", false);
|
|
||||||
xhr.send();
|
|
||||||
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins = JSON.parse(xhr.responseText);
|
|
||||||
const headlessSync = plugins.find(
|
const headlessSync = plugins.find(
|
||||||
(p) => p.id === "headless-sync" && p.bundledPluginId,
|
(p) => p.id === "headless-sync" && p.bundledPluginId,
|
||||||
);
|
);
|
||||||
|
|
@ -164,6 +184,24 @@ function initCoreSyncGuard() {
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCoreSyncGuardFallback() {
|
||||||
|
const vaultId = window.__currentVaultId;
|
||||||
|
|
||||||
|
if (!vaultId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.open("GET", "/api/plugins", false);
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
applyCoreSyncGuard(JSON.parse(xhr.responseText));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[ignis] Failed to init core sync guard:", e);
|
console.warn("[ignis] Failed to init core sync guard:", e);
|
||||||
}
|
}
|
||||||
|
|
@ -171,11 +209,22 @@ function initCoreSyncGuard() {
|
||||||
|
|
||||||
export function initialize() {
|
export function initialize() {
|
||||||
resolveVaultId();
|
resolveVaultId();
|
||||||
initVaultConfig();
|
|
||||||
resolveWorkspaceName();
|
resolveWorkspaceName();
|
||||||
initVaultList();
|
|
||||||
initMetadataCache();
|
const bootstrap = fetchBootstrap();
|
||||||
initCoreSyncGuard();
|
|
||||||
|
if (bootstrap) {
|
||||||
|
applyVaultInfo(bootstrap.vault);
|
||||||
|
window.__vaultList = bootstrap.vaultList;
|
||||||
|
applyTree(bootstrap.tree);
|
||||||
|
applyCoreSyncGuard(bootstrap.plugins);
|
||||||
|
} else {
|
||||||
|
initVaultConfigFallback();
|
||||||
|
initVaultListFallback();
|
||||||
|
initMetadataCacheFallback();
|
||||||
|
initCoreSyncGuardFallback();
|
||||||
|
}
|
||||||
|
|
||||||
installRequestUrlShim();
|
installRequestUrlShim();
|
||||||
initWorkspacePatch();
|
initWorkspacePatch();
|
||||||
initPluginPrompt();
|
initPluginPrompt();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue