mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
add server settings UI and enforcement
This commit is contained in:
@@ -213,7 +213,6 @@ const wss = setupWebSocket(server, {
|
||||
getVaultPath: config.getVaultPath,
|
||||
originAllowlist: settings.get("wsOrigins"),
|
||||
});
|
||||
app.set("wss", wss);
|
||||
wireDemoWebSocket(server);
|
||||
|
||||
async function gracefulShutdown(signal) {
|
||||
|
||||
@@ -100,16 +100,20 @@ router.post("/", async (req, res) => {
|
||||
return res.status(400).json({ error: "Missing url" });
|
||||
}
|
||||
|
||||
const proxyMode = settings.get("proxyMode");
|
||||
|
||||
if (proxyMode === "disabled") {
|
||||
return res.status(403).json({ error: "Proxy is disabled" });
|
||||
}
|
||||
|
||||
try {
|
||||
await assertPublicUrl(url);
|
||||
} catch (e) {
|
||||
return res.status(e.statusCode || 400).json({ error: e.message });
|
||||
}
|
||||
|
||||
// When a host allowlist is defined , the proxy only reaches those hosts.
|
||||
const allowlist = settings.get("proxyAllowlist");
|
||||
|
||||
if (allowlist.length > 0) {
|
||||
if (proxyMode === "allowlist") {
|
||||
const allowlist = settings.get("proxyAllowlist");
|
||||
const host = new URL(url).hostname;
|
||||
|
||||
if (!allowlist.includes(host)) {
|
||||
|
||||
@@ -12,11 +12,21 @@ const NUMBER_KEYS = [
|
||||
"writeCoalesceMs",
|
||||
"maxBodyBytes",
|
||||
];
|
||||
const LIST_KEYS = ["proxyAllowlist", "wsOrigins"];
|
||||
const LIST_KEYS = ["proxyAllowlist"];
|
||||
|
||||
function validate(body) {
|
||||
const clean = {};
|
||||
|
||||
if (body.proxyMode !== undefined) {
|
||||
if (!settings.PROXY_MODES.includes(body.proxyMode)) {
|
||||
throw new Error(
|
||||
`proxyMode must be one of: ${settings.PROXY_MODES.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
clean.proxyMode = body.proxyMode;
|
||||
}
|
||||
|
||||
for (const key of NUMBER_KEYS) {
|
||||
if (body[key] === undefined) {
|
||||
continue;
|
||||
@@ -57,14 +67,8 @@ function validate(body) {
|
||||
return clean;
|
||||
}
|
||||
|
||||
function applySettings(effective, req) {
|
||||
function applySettings(effective) {
|
||||
writeCoalescer.configure({ writeCoalesceMs: effective.writeCoalesceMs });
|
||||
|
||||
const wss = req.app.get("wss");
|
||||
|
||||
if (wss && typeof wss.setOriginAllowlist === "function") {
|
||||
wss.setOriginAllowlist(effective.wsOrigins);
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
@@ -81,7 +85,7 @@ router.post("/", (req, res) => {
|
||||
}
|
||||
|
||||
const effective = settings.update(clean);
|
||||
applySettings(effective, req);
|
||||
applySettings(effective);
|
||||
|
||||
// Cache sizes ride in the bootstrap response; clear it so the next page load picks up new values.
|
||||
bootstrapRoutes.invalidateAll();
|
||||
|
||||
@@ -12,13 +12,20 @@ const DEFAULTS = {
|
||||
inputCacheTtlMs: 5 * 60 * 1000,
|
||||
writeCoalesceMs: 5000,
|
||||
maxBodyBytes: 50 * 1024 * 1024,
|
||||
// Empty arrays mean "no restriction": any proxy host, any WS origin.
|
||||
// "any" reaches any public host, "allowlist" restricts to proxyAllowlist, "disabled" blocks all proxying.
|
||||
proxyMode: "any",
|
||||
// Empty allows any public host.
|
||||
proxyAllowlist: [],
|
||||
wsOrigins: [],
|
||||
};
|
||||
|
||||
const PROXY_MODES = ["any", "allowlist", "disabled"];
|
||||
|
||||
const KEYS = Object.keys(DEFAULTS);
|
||||
|
||||
// Env vars only; never persisted to the settings file.
|
||||
const ENV_ONLY_KEYS = ["wsOrigins"];
|
||||
|
||||
// Hard ceiling for request bodies.
|
||||
const MAX_BODY_BACKSTOP = 500 * 1024 * 1024;
|
||||
|
||||
@@ -56,6 +63,10 @@ function loadFile() {
|
||||
const clean = {};
|
||||
|
||||
for (const key of KEYS) {
|
||||
if (ENV_ONLY_KEYS.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed[key] !== undefined) {
|
||||
clean[key] = parsed[key];
|
||||
}
|
||||
@@ -80,7 +91,7 @@ function get(key) {
|
||||
// Merge validated changes into the persisted file and return the new effective settings.
|
||||
function update(partial) {
|
||||
for (const [key, value] of Object.entries(partial)) {
|
||||
if (KEYS.includes(key)) {
|
||||
if (KEYS.includes(key) && !ENV_ONLY_KEYS.includes(key)) {
|
||||
fileOverrides[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -91,4 +102,13 @@ function update(partial) {
|
||||
return getAll();
|
||||
}
|
||||
|
||||
module.exports = { DEFAULTS, KEYS, MAX_BODY_BACKSTOP, getAll, get, update };
|
||||
module.exports = {
|
||||
DEFAULTS,
|
||||
KEYS,
|
||||
ENV_ONLY_KEYS,
|
||||
PROXY_MODES,
|
||||
MAX_BODY_BACKSTOP,
|
||||
getAll,
|
||||
get,
|
||||
update,
|
||||
};
|
||||
|
||||
@@ -51,4 +51,4 @@ function stopDemoGuards() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { startDemoGuards, stopDemoGuards };
|
||||
module.exports = { startDemoGuards, stopDemoGuards, isDemoMode };
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const { Setting } = require("obsidian");
|
||||
const { Setting, Notice } = require("obsidian");
|
||||
const { isDemoMode } = require("../demo-guards");
|
||||
const { stripBuildMetadata, isNewer } = require("../util/version");
|
||||
const { ListEditorModal } = require("./list-editor-modal");
|
||||
|
||||
const GITHUB_URL = "https://github.com/Nystik-gh/ignis";
|
||||
const GITHUB_API_LATEST =
|
||||
@@ -8,11 +11,6 @@ function getVersion() {
|
||||
return window.__ignis?.version || "unknown";
|
||||
}
|
||||
|
||||
// SemVer build metadata (`+xyz`) is informational and ignored for precedence.
|
||||
function stripBuildMetadata(version) {
|
||||
return (version || "").split("+")[0];
|
||||
}
|
||||
|
||||
async function checkForUpdate(currentVersion) {
|
||||
try {
|
||||
const res = await fetch(GITHUB_API_LATEST);
|
||||
@@ -25,7 +23,7 @@ async function checkForUpdate(currentVersion) {
|
||||
const latest = stripBuildMetadata(data.tag_name?.replace(/^v/, ""));
|
||||
const current = stripBuildMetadata(currentVersion);
|
||||
|
||||
if (latest && latest !== current) {
|
||||
if (isNewer(latest, current)) {
|
||||
return { version: latest, url: data.html_url };
|
||||
}
|
||||
|
||||
@@ -88,6 +86,7 @@ function display(containerEl, app) {
|
||||
});
|
||||
|
||||
addServerStatus(containerEl);
|
||||
addServerSettings(containerEl, app);
|
||||
}
|
||||
|
||||
const STATUS_LABELS = {
|
||||
@@ -102,10 +101,26 @@ const STATUS_DOT_CLASSES = {
|
||||
closed: "ignis-status-disconnected",
|
||||
};
|
||||
|
||||
// Obsidian's grouped-settings container. The .setting-group > .setting-items
|
||||
// structure renders its child settings inside one shared box, with an optional
|
||||
// heading as a sibling above the box. The shipped stylesheet supplies the
|
||||
// styling from theme variables, so the rows need no custom CSS.
|
||||
function createSettingGroup(containerEl, heading) {
|
||||
const group = containerEl.createDiv("setting-group");
|
||||
|
||||
if (heading) {
|
||||
new Setting(group).setName(heading).setHeading();
|
||||
}
|
||||
|
||||
return group.createDiv("setting-items");
|
||||
}
|
||||
|
||||
function addServerStatus(containerEl) {
|
||||
const ws = window.__ignis.ws;
|
||||
|
||||
const setting = new Setting(containerEl).setName("Server status");
|
||||
const items = createSettingGroup(containerEl);
|
||||
|
||||
const setting = new Setting(items).setName("Server status");
|
||||
|
||||
const dotEl = setting.controlEl.createEl("span", {
|
||||
cls: "ignis-status-dot",
|
||||
@@ -138,4 +153,204 @@ function addServerStatus(containerEl) {
|
||||
});
|
||||
}
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
const MINUTE = 60 * 1000;
|
||||
|
||||
function addServerSettings(containerEl, app) {
|
||||
if (isDemoMode()) {
|
||||
const items = createSettingGroup(containerEl);
|
||||
|
||||
new Setting(items)
|
||||
.setName("Server settings")
|
||||
.setDesc("Server settings are disabled in demo mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
const loading = containerEl.createEl("p", {
|
||||
text: "Loading server settings...",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
fetch("/api/settings")
|
||||
.then((res) => (res.ok ? res.json() : Promise.reject(res)))
|
||||
.then((current) => {
|
||||
loading.remove();
|
||||
renderServerSettings(containerEl, current, app);
|
||||
})
|
||||
.catch(() => {
|
||||
loading.setText("Failed to load server settings.");
|
||||
});
|
||||
}
|
||||
|
||||
function renderServerSettings(containerEl, current, app) {
|
||||
const caching = createSettingGroup(containerEl, "Caching");
|
||||
|
||||
numberField(caching, {
|
||||
name: "Content cache (MB)",
|
||||
desc: "Browser cache of file content. Applies after reload.",
|
||||
value: Math.round(current.contentCacheBytes / MB),
|
||||
key: "contentCacheBytes",
|
||||
toStored: (n) => n * MB,
|
||||
});
|
||||
|
||||
numberField(caching, {
|
||||
name: "Input cache (MB)",
|
||||
desc: "Cache for files picked for import. Applies after reload.",
|
||||
value: Math.round(current.inputCacheBytes / MB),
|
||||
key: "inputCacheBytes",
|
||||
toStored: (n) => n * MB,
|
||||
});
|
||||
|
||||
numberField(caching, {
|
||||
name: "Input cache TTL (minutes)",
|
||||
desc: "How long picked files stay cached. Applies after reload.",
|
||||
value: Math.round(current.inputCacheTtlMs / MINUTE),
|
||||
key: "inputCacheTtlMs",
|
||||
toStored: (n) => n * MINUTE,
|
||||
});
|
||||
|
||||
const security = createSettingGroup(containerEl, "Security");
|
||||
|
||||
numberField(security, {
|
||||
name: "Max request body (MB)",
|
||||
desc: "Largest request the server accepts.",
|
||||
value: Math.round(current.maxBodyBytes / MB),
|
||||
key: "maxBodyBytes",
|
||||
toStored: (n) => n * MB,
|
||||
});
|
||||
|
||||
proxyAccessField(security, current, app);
|
||||
|
||||
const advanced = createSettingGroup(containerEl, "Advanced");
|
||||
|
||||
numberField(advanced, {
|
||||
name: "Write coalesce window (ms)",
|
||||
desc: "Debounce window for rapid writes on slow filesystems. 0 disables.",
|
||||
value: current.writeCoalesceMs,
|
||||
key: "writeCoalesceMs",
|
||||
toStored: (n) => n,
|
||||
});
|
||||
}
|
||||
|
||||
// Persist a single setting. The server validates, applies the live ones, and saves.
|
||||
async function saveSetting(partial) {
|
||||
try {
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(partial),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || "Save failed");
|
||||
}
|
||||
} catch (e) {
|
||||
new Notice(`Failed to save setting: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function numberField(containerEl, { name, desc, value, key, toStored }) {
|
||||
new Setting(containerEl)
|
||||
.setName(name)
|
||||
.setDesc(desc)
|
||||
.addText((text) => {
|
||||
text.setValue(String(value));
|
||||
|
||||
// Commit on blur or Enter, the way a native number setting behaves.
|
||||
const commit = () => {
|
||||
const n = parseInt(text.getValue(), 10);
|
||||
|
||||
if (Number.isInteger(n) && n >= 0) {
|
||||
saveSetting({ [key]: toStored(n) });
|
||||
}
|
||||
};
|
||||
|
||||
text.inputEl.addEventListener("blur", commit);
|
||||
text.inputEl.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
commit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Proxy access mode plus the allowlist row, which only shows in "allowlist" mode.
|
||||
function proxyAccessField(parent, current, app) {
|
||||
let mode = current.proxyMode || "any";
|
||||
|
||||
const setting = new Setting(parent)
|
||||
.setName("Proxy access")
|
||||
.setDesc(
|
||||
"Which external hosts Obsidian may reach through the server's CORS proxy.",
|
||||
);
|
||||
|
||||
const allowlistSetting = listField(parent, {
|
||||
name: "Proxy host allowlist",
|
||||
desc: "Hostnames the proxy may reach, matched exactly.",
|
||||
value: current.proxyAllowlist,
|
||||
key: "proxyAllowlist",
|
||||
app,
|
||||
modal: {
|
||||
placeholder: "api.example.com",
|
||||
emptyNote: "No hosts yet.",
|
||||
recommended: {
|
||||
note: "Restricting the proxy stops Obsidian's plugin and theme browser and updates from working unless their hosts are allowed.",
|
||||
hosts: ["releases.obsidian.md", "github.com", "raw.githubusercontent.com"],
|
||||
buttonText: "Add recommended hosts",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const applyVisibility = () => {
|
||||
allowlistSetting.settingEl.style.display =
|
||||
mode === "allowlist" ? "" : "none";
|
||||
};
|
||||
|
||||
setting.addDropdown((dd) => {
|
||||
dd.addOption("any", "Any public host");
|
||||
dd.addOption("allowlist", "Allowlist only");
|
||||
dd.addOption("disabled", "Disabled");
|
||||
dd.setValue(mode);
|
||||
|
||||
dd.onChange((value) => {
|
||||
mode = value;
|
||||
saveSetting({ proxyMode: value });
|
||||
applyVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
applyVisibility();
|
||||
}
|
||||
|
||||
function listField(containerEl, { name, desc, value, key, app, modal }) {
|
||||
let current = [...(value || [])];
|
||||
|
||||
const setting = new Setting(containerEl).setName(name).setDesc(desc);
|
||||
|
||||
const setLabel = (btn) =>
|
||||
btn.setButtonText(current.length ? `Edit (${current.length})` : "Edit");
|
||||
|
||||
setting.addButton((btn) => {
|
||||
setLabel(btn);
|
||||
|
||||
btn.onClick(() => {
|
||||
new ListEditorModal(app, {
|
||||
title: name,
|
||||
placeholder: modal.placeholder,
|
||||
emptyNote: modal.emptyNote,
|
||||
recommended: modal.recommended,
|
||||
values: current,
|
||||
onChange: (next) => {
|
||||
current = next;
|
||||
saveSetting({ [key]: current });
|
||||
setLabel(btn);
|
||||
},
|
||||
}).open();
|
||||
});
|
||||
});
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
module.exports = { display };
|
||||
|
||||
134
packages/bridge/src/settings/list-editor-modal.js
Normal file
134
packages/bridge/src/settings/list-editor-modal.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const { Modal, Setting, Notice } = require("obsidian");
|
||||
|
||||
// Modal editor for a list of string entries (the proxy host allowlist).
|
||||
class ListEditorModal extends Modal {
|
||||
constructor(app, opts) {
|
||||
super(app);
|
||||
this.opts = opts;
|
||||
this.values = [...(opts.values || [])];
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.titleEl.setText(this.opts.title);
|
||||
|
||||
if (this.opts.recommended) {
|
||||
new Setting(this.contentEl)
|
||||
.setDesc(this.opts.recommended.note)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText(
|
||||
this.opts.recommended.buttonText || "Add recommended",
|
||||
)
|
||||
.onClick(() => this.addRecommended()),
|
||||
);
|
||||
}
|
||||
|
||||
this.listEl = this.contentEl.createDiv("ignis-list-editor");
|
||||
this.renderList();
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Add entry")
|
||||
.addText((text) => {
|
||||
this.input = text;
|
||||
text.setPlaceholder(this.opts.placeholder || "");
|
||||
|
||||
text.inputEl.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.addCurrent();
|
||||
}
|
||||
});
|
||||
})
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Add")
|
||||
.setCta()
|
||||
.onClick(() => this.addCurrent()),
|
||||
);
|
||||
}
|
||||
|
||||
addEntry(entry) {
|
||||
if (this.values.includes(entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.values.push(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
addCurrent() {
|
||||
const entry = this.input.getValue().trim();
|
||||
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.addEntry(entry)) {
|
||||
new Notice("That entry is already in the list.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.input.setValue("");
|
||||
this.input.inputEl.focus();
|
||||
this.commit();
|
||||
this.renderList();
|
||||
}
|
||||
|
||||
addRecommended() {
|
||||
let added = 0;
|
||||
|
||||
for (const host of this.opts.recommended.hosts) {
|
||||
if (this.addEntry(host)) {
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
this.commit();
|
||||
this.renderList();
|
||||
}
|
||||
|
||||
new Notice(
|
||||
added > 0
|
||||
? `Added ${added} host${added === 1 ? "" : "s"}.`
|
||||
: "All recommended hosts are already in the list.",
|
||||
);
|
||||
}
|
||||
|
||||
remove(entry) {
|
||||
this.values = this.values.filter((v) => v !== entry);
|
||||
this.commit();
|
||||
this.renderList();
|
||||
}
|
||||
|
||||
renderList() {
|
||||
this.listEl.empty();
|
||||
|
||||
if (this.values.length === 0) {
|
||||
this.listEl.createDiv({
|
||||
text: this.opts.emptyNote,
|
||||
cls: "ignis-list-empty",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of this.values) {
|
||||
new Setting(this.listEl).setName(entry).addExtraButton((btn) =>
|
||||
btn
|
||||
.setIcon("trash-2")
|
||||
.setTooltip("Remove")
|
||||
.onClick(() => this.remove(entry)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
commit() {
|
||||
this.opts.onChange([...this.values]);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ListEditorModal };
|
||||
39
packages/bridge/src/util/version.js
Normal file
39
packages/bridge/src/util/version.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Version comparison helpers for the update check.
|
||||
|
||||
// SemVer build metadata (`+xyz`) is informational and ignored for precedence.
|
||||
function stripBuildMetadata(version) {
|
||||
return (version || "").split("+")[0];
|
||||
}
|
||||
|
||||
// Parse X.Y.Z to [major, minor, patch], or null when it isn't three integers.
|
||||
function parseSemver(version) {
|
||||
const parts = (version || "").split(".");
|
||||
|
||||
if (parts.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nums = parts.slice(0, 3).map((p) => parseInt(p, 10));
|
||||
|
||||
return nums.some((n) => !Number.isInteger(n)) ? null : nums;
|
||||
}
|
||||
|
||||
// True only when latest is strictly newer than current.
|
||||
function isNewer(latest, current) {
|
||||
const a = parseSemver(latest);
|
||||
const b = parseSemver(current);
|
||||
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return a[i] > b[i];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { stripBuildMetadata, parseSemver, isNewer };
|
||||
@@ -141,3 +141,18 @@
|
||||
font-size: var(--font-ui-small);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ignis-list-editor {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
padding: 0 var(--size-4-3);
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
.ignis-list-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
padding: var(--size-4-3) 0;
|
||||
}
|
||||
|
||||
@@ -14,14 +14,10 @@ function setupWebSocket(server, opts = {}) {
|
||||
throw new Error("setupWebSocket: opts.getVaultPath is required");
|
||||
}
|
||||
|
||||
let originSet = toOriginSet(originAllowlist);
|
||||
const originSet = toOriginSet(originAllowlist);
|
||||
|
||||
const wss = new WebSocketServer({ server, path: "/ws" });
|
||||
|
||||
wss.setOriginAllowlist = function (list) {
|
||||
originSet = toOriginSet(list);
|
||||
};
|
||||
|
||||
// Global message handlers: type -> handler(msg, ws).
|
||||
wss.messageHandlers = new Map();
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ export class ContentCache {
|
||||
this._maxSize = maxSize;
|
||||
}
|
||||
|
||||
setMaxSize(maxSize) {
|
||||
this._maxSize = maxSize;
|
||||
|
||||
while (this._currentSize > this._maxSize && this._cache.size > 0) {
|
||||
this._evictOne();
|
||||
}
|
||||
}
|
||||
|
||||
has(path) {
|
||||
return this._cache.has(this._normalize(path));
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import { normalize } from "../util/path.js";
|
||||
|
||||
const MAX_SIZE = 200 * 1024 * 1024;
|
||||
const TTL_MS = 5 * 60 * 1000;
|
||||
let MAX_SIZE = 200 * 1024 * 1024;
|
||||
let TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
const cache = new Map(); // path -> { data, size, createdAt }
|
||||
let currentSize = 0;
|
||||
@@ -112,6 +112,20 @@ export function inputCacheClear() {
|
||||
currentSize = 0;
|
||||
}
|
||||
|
||||
export function setInputCacheLimits({ maxSize, ttlMs }) {
|
||||
if (Number.isFinite(maxSize)) {
|
||||
MAX_SIZE = maxSize;
|
||||
|
||||
while (currentSize > MAX_SIZE && cache.size > 0) {
|
||||
evictOldest();
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(ttlMs)) {
|
||||
TTL_MS = ttlMs;
|
||||
}
|
||||
}
|
||||
|
||||
export function isInputCachePath(path) {
|
||||
const norm = normalize(path);
|
||||
return norm.startsWith(".obsidian/imports/");
|
||||
|
||||
@@ -8,11 +8,26 @@ import {
|
||||
initWorkspacePatch,
|
||||
} from "./workspace.js";
|
||||
import { prefetchVaultContent } from "./fs/indexer-prefetch.js";
|
||||
import { setInputCacheLimits } from "./fs/input-cache.js";
|
||||
import { autoTrustDemoVaults, maybeProvisionDemoVault } from "./demo.js";
|
||||
import { initNativeMenuGuard } from "./native-menu-guard.js";
|
||||
|
||||
let bootstrapVirtualPlugins = [];
|
||||
|
||||
// Cache sizes come from the bootstrap response and are applied at page load.
|
||||
// The server owns the rest of the settings and applies them on its side.
|
||||
function applyServerSettings(s) {
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isFinite(s.contentCacheBytes)) {
|
||||
fsShim._contentCache.setMaxSize(s.contentCacheBytes);
|
||||
}
|
||||
|
||||
setInputCacheLimits({ maxSize: s.inputCacheBytes, ttlMs: s.inputCacheTtlMs });
|
||||
}
|
||||
|
||||
export function getBootstrapVirtualPlugins() {
|
||||
return bootstrapVirtualPlugins;
|
||||
}
|
||||
@@ -212,6 +227,7 @@ export function initialize() {
|
||||
applyTree(bootstrap.tree);
|
||||
applyCoreSyncGuard(bootstrap.plugins);
|
||||
bootstrapVirtualPlugins = bootstrap.virtualPlugins || [];
|
||||
applyServerSettings(bootstrap.settings);
|
||||
|
||||
// Race the indexer: batch-fetch text content into ContentCache so
|
||||
// Obsidian's startup indexing reads hit the cache instead of the network.
|
||||
|
||||
Reference in New Issue
Block a user