mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
add index pre-fetch
This commit is contained in:
@@ -479,6 +479,58 @@ router.post("/utimes", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/fs/batch-read { paths, vault } - bulk read text file contents
|
||||||
|
// Used by the indexer pre-fetcher to avoid N round trips during startup.
|
||||||
|
router.post("/batch-read", async (req, res) => {
|
||||||
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
|
||||||
|
if (!vaultRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = Array.isArray(req.body?.paths) ? req.body.paths : [];
|
||||||
|
|
||||||
|
if (paths.length === 0) {
|
||||||
|
return res.json({ files: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
paths.map(async (relPath) => {
|
||||||
|
const resolved = resolveVaultPath(vaultRoot, relPath);
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffered = getPending(resolved);
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
if (typeof buffered.data === "string") {
|
||||||
|
files[relPath] = buffered.data;
|
||||||
|
} else if (
|
||||||
|
buffered.encoding === "utf8" ||
|
||||||
|
buffered.encoding === "utf-8"
|
||||||
|
) {
|
||||||
|
files[relPath] = buffered.data.toString("utf-8");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fs.promises.readFile(resolved, "utf-8");
|
||||||
|
files[relPath] = data;
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files silently. The client falls back to a
|
||||||
|
// normal readFile when a path isn't in the response.
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ files });
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/fs/tree?path=...&vault=... returns full recursive file tree with metadata
|
// GET /api/fs/tree?path=...&vault=... returns full recursive file tree with metadata
|
||||||
router.get("/tree", async (req, res) => {
|
router.get("/tree", async (req, res) => {
|
||||||
const vaultRoot = getVaultRoot(req, res);
|
const vaultRoot = getVaultRoot(req, res);
|
||||||
|
|||||||
113
src/shims/fs/indexer-prefetch.js
Normal file
113
src/shims/fs/indexer-prefetch.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// Eager batch pre-fetch of vault content into ContentCache.
|
||||||
|
//
|
||||||
|
// Fired once after the metadata cache is populated. Iterates the tree in
|
||||||
|
// directory-traversal order and pulls text file contents in batches via
|
||||||
|
// /api/fs/batch-read. Caps at MAX_BYTES so it doesn't thrash the LRU.
|
||||||
|
// Drops content directly into ContentCache; the indexer hits the cache
|
||||||
|
// instead of fetching each file individually.
|
||||||
|
|
||||||
|
const TEXT_EXTENSIONS = new Set([
|
||||||
|
".md", ".markdown", ".txt", ".json", ".csv",
|
||||||
|
".css", ".js", ".ts", ".tsx", ".mjs", ".cjs",
|
||||||
|
".html", ".xml", ".yaml", ".yml", ".toml",
|
||||||
|
".svg",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MAX_BYTES = 30 * 1024 * 1024; // 30 MB
|
||||||
|
const MAX_FILE_BYTES = 512 * 1024; // skip files larger than 512 KB
|
||||||
|
const BATCH_SIZE = 50;
|
||||||
|
|
||||||
|
function isTextPath(path) {
|
||||||
|
const dot = path.lastIndexOf(".");
|
||||||
|
|
||||||
|
if (dot < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TEXT_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPrefetchTargets(tree) {
|
||||||
|
const paths = [];
|
||||||
|
let bytes = 0;
|
||||||
|
|
||||||
|
// Iterate in tree key order, which already matches directory traversal
|
||||||
|
// because the server's walk emits parent-before-children.
|
||||||
|
for (const [path, entry] of Object.entries(tree)) {
|
||||||
|
if (entry.type !== "file") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTextPath(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = entry.size || 0;
|
||||||
|
|
||||||
|
if (size === 0 || size > MAX_FILE_BYTES) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes + size > MAX_BYTES) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.push(path);
|
||||||
|
bytes += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { paths, bytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBatch(vaultId, paths) {
|
||||||
|
const res = await fetch("/api/fs/batch-read", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ vault: vaultId, paths }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("batch-read failed: " + res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prefetchVaultContent(vaultId, tree, contentCache) {
|
||||||
|
if (!vaultId || !tree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { paths, bytes } = selectPrefetchTargets(tree);
|
||||||
|
|
||||||
|
if (paths.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = Date.now();
|
||||||
|
let cached = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < paths.length; i += BATCH_SIZE) {
|
||||||
|
const batch = paths.slice(i, i + BATCH_SIZE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetchBatch(vaultId, batch);
|
||||||
|
|
||||||
|
for (const [path, content] of Object.entries(result.files || {})) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
contentCache.set(path, content);
|
||||||
|
cached++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[ignis] Prefetch batch failed:", e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms = Date.now() - t0;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[ignis] Prefetched ${cached}/${paths.length} files (${(bytes / 1024).toFixed(0)} KB) in ${ms}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { vaultService } from "../services/vault-service.js";
|
|||||||
import { showPluginInstallDialog } from "../ui/bootstrap.js";
|
import { showPluginInstallDialog } from "../ui/bootstrap.js";
|
||||||
import { registerReadTransform } from "./fs/read-transforms.js";
|
import { registerReadTransform } from "./fs/read-transforms.js";
|
||||||
import { resolveWorkspaceName, initWorkspacePatch } from "./workspace.js";
|
import { resolveWorkspaceName, initWorkspacePatch } from "./workspace.js";
|
||||||
|
import { prefetchVaultContent } from "./fs/indexer-prefetch.js";
|
||||||
|
|
||||||
function resolveVaultId() {
|
function resolveVaultId() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -218,6 +219,14 @@ export function initialize() {
|
|||||||
window.__vaultList = bootstrap.vaultList;
|
window.__vaultList = bootstrap.vaultList;
|
||||||
applyTree(bootstrap.tree);
|
applyTree(bootstrap.tree);
|
||||||
applyCoreSyncGuard(bootstrap.plugins);
|
applyCoreSyncGuard(bootstrap.plugins);
|
||||||
|
|
||||||
|
// Race the indexer: batch-fetch text content into ContentCache so
|
||||||
|
// Obsidian's startup indexing reads hit the cache instead of the network.
|
||||||
|
prefetchVaultContent(
|
||||||
|
window.__currentVaultId,
|
||||||
|
bootstrap.tree,
|
||||||
|
fsShim._contentCache,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
initVaultConfigFallback();
|
initVaultConfigFallback();
|
||||||
initVaultListFallback();
|
initVaultListFallback();
|
||||||
|
|||||||
Reference in New Issue
Block a user