From 4a4d9044209533b7ca80261c21dd542502b16d56 Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sun, 29 Mar 2026 22:03:42 +0200 Subject: [PATCH] improve sync input workaround --- src/shims/electron/remote/dialog.js | 23 +++--- src/shims/fs/fd.js | 19 ++++- src/shims/fs/input-cache.js | 123 ++++++++++++++++++++++++++++ src/shims/fs/promises.js | 24 +++++- src/shims/fs/sync.js | 39 +++++++++ 5 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 src/shims/fs/input-cache.js diff --git a/src/shims/electron/remote/dialog.js b/src/shims/electron/remote/dialog.js index 514ae3c..4e94a74 100644 --- a/src/shims/electron/remote/dialog.js +++ b/src/shims/electron/remote/dialog.js @@ -3,7 +3,7 @@ import { showConfirmDialog, showPromptDialog, } from "../../../ui/bootstrap.js"; -import { transport } from "../../fs/transport.js"; +import { inputCacheSet, inputCacheDelete } from "../../fs/input-cache.js"; const IMPORTS_DIR = ".obsidian/imports"; const STAGED_TTL_MS = 120_000; // 2 minutes @@ -24,7 +24,7 @@ function clearStagedFiles() { console.log("[shim:dialog] Clearing expired staged files"); for (const p of staged.paths) { - transport.unlink(p.replace(/^\//, "")).catch(() => {}); + inputCacheDelete(p.replace(/^\//, "")); } staged = { paths: [], fingerprint: null, timestamp: 0 }; @@ -72,12 +72,12 @@ function pickFiles(accept, multiple) { }); } -async function uploadToImports(file) { +async function cacheToImports(file) { const arrayBuffer = await file.arrayBuffer(); const bytes = new Uint8Array(arrayBuffer); const targetPath = IMPORTS_DIR + "/" + file.name; - await transport.writeFile(targetPath, bytes); + inputCacheSet(targetPath, bytes); return "/" + targetPath; } @@ -96,7 +96,7 @@ async function startWorkaroundFlow(options, fingerprint) { const paths = []; for (const file of files) { - const vaultPath = await uploadToImports(file); + const vaultPath = await cacheToImports(file); paths.push(vaultPath); } @@ -108,7 +108,7 @@ async function startWorkaroundFlow(options, fingerprint) { await showMessageDialog( "Files Ready", - `Uploaded: ${names}\n\nPlease retry the action that brought you here. ` + + `Staged: ${names}\n\nPlease retry the action that brought you here. ` + "The files will be provided automatically.", ); } @@ -134,11 +134,11 @@ export const dialogShim = { const filePaths = []; for (const file of files) { - const vaultPath = await uploadToImports(file); + const vaultPath = await cacheToImports(file); filePaths.push(vaultPath); } - console.log("[shim:dialog] showOpenDialog - uploaded:", filePaths); + console.log("[shim:dialog] showOpenDialog - cached:", filePaths); return { canceled: false, filePaths }; }, @@ -187,9 +187,10 @@ export const dialogShim = { showConfirmDialog( "Feature Not Available", "This action requires a native file picker which is not available in the browser.", - "A workaround is available: upload your file first, then retry the action. " + - "Would you like to proceed?", - "Upload File", + "A workaround is available: select your files first, then retry the action. " + + "They will be provided automatically.\n\n" + + "Note: individual files must be under 200 MB.", + "Select Files", ).then((confirmed) => { if (confirmed) { startWorkaroundFlow(options, callerFingerprint); diff --git a/src/shims/fs/fd.js b/src/shims/fs/fd.js index 56f53ca..570de8c 100644 --- a/src/shims/fs/fd.js +++ b/src/shims/fs/fd.js @@ -2,11 +2,26 @@ // Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek // around files without loading them via readFileSync upfront. +import { isInputCachePath, inputCacheGet } from "./input-cache.js"; + let nextFd = 100; const openFiles = new Map(); export function createFdOps(metadataCache, contentCache, transport) { function ensureData(path) { + // Check input cache first for files picked via browser file dialogs. + if (isInputCachePath(path)) { + const inputData = inputCacheGet(path); + + if (inputData !== null) { + if (typeof inputData === "string") { + return new TextEncoder().encode(inputData); + } + + return inputData; + } + } + const cached = contentCache.get(path); if (cached !== null) { @@ -40,7 +55,9 @@ export function createFdOps(metadataCache, contentCache, transport) { // --- Sync --- function openSync(path, flags, mode) { - if (!metadataCache.has(path)) { + const hasInCache = isInputCachePath(path) && inputCacheGet(path) !== null; + + if (!hasInCache && !metadataCache.has(path)) { const err = new Error( `ENOENT: no such file or directory, open '${path}'`, ); diff --git a/src/shims/fs/input-cache.js b/src/shims/fs/input-cache.js new file mode 100644 index 0000000..1645f9f --- /dev/null +++ b/src/shims/fs/input-cache.js @@ -0,0 +1,123 @@ +// Dedicated cache for files picked via browser file dialogs. +// Avoids server round trips for input-only files (e.g., importer plugin). +// +// - 200MB size limit (higher than content cache; import batches can be large) +// - 5-minute TTL per entry +// - Entries kept until TTL expires (plugins may read the same file multiple times) + +const MAX_SIZE = 200 * 1024 * 1024; +const TTL_MS = 5 * 60 * 1000; + +const cache = new Map(); // path -> { data, size, createdAt } +let currentSize = 0; + +function normalize(p) { + return (p || "") + .replace(/\\/g, "/") + .replace(/^\/+/, "") + .replace(/\/+$/, ""); +} + +function evictExpired() { + const now = Date.now(); + + for (const [key, entry] of cache) { + if (now - entry.createdAt > TTL_MS) { + currentSize -= entry.size; + cache.delete(key); + } + } +} + +function evictOldest() { + let oldest = null; + let oldestTime = Infinity; + + for (const [key, entry] of cache) { + if (entry.createdAt < oldestTime) { + oldest = key; + oldestTime = entry.createdAt; + } + } + + if (oldest) { + currentSize -= cache.get(oldest).size; + cache.delete(oldest); + } +} + +export function inputCacheHas(path) { + const norm = normalize(path); + const entry = cache.get(norm); + + if (!entry) { + return false; + } + + if (Date.now() - entry.createdAt > TTL_MS) { + currentSize -= entry.size; + cache.delete(norm); + return false; + } + + return true; +} + +export function inputCacheGet(path) { + const norm = normalize(path); + const entry = cache.get(norm); + + if (!entry) { + return null; + } + + if (Date.now() - entry.createdAt > TTL_MS) { + currentSize -= entry.size; + cache.delete(norm); + return null; + } + + return entry.data; +} + +export function inputCacheSet(path, data) { + const norm = normalize(path); + const size = data ? data.length || data.byteLength || 0 : 0; + + // Remove existing entry if replacing + if (cache.has(norm)) { + currentSize -= cache.get(norm).size; + cache.delete(norm); + } + + // Evict expired entries first + evictExpired(); + + // Evict oldest entries if still over limit + while (currentSize + size > MAX_SIZE && cache.size > 0) { + evictOldest(); + } + + cache.set(norm, { data, size, createdAt: Date.now() }); + currentSize += size; +} + +export function inputCacheDelete(path) { + const norm = normalize(path); + const entry = cache.get(norm); + + if (entry) { + currentSize -= entry.size; + cache.delete(norm); + } +} + +export function inputCacheClear() { + cache.clear(); + currentSize = 0; +} + +export function isInputCachePath(path) { + const norm = normalize(path); + return norm.startsWith(".obsidian/imports/"); +} diff --git a/src/shims/fs/promises.js b/src/shims/fs/promises.js index 2046980..8155fb7 100644 --- a/src/shims/fs/promises.js +++ b/src/shims/fs/promises.js @@ -1,4 +1,5 @@ import { markLocalOp } from "./echo-guard.js"; +import { isInputCachePath, inputCacheGet } from "./input-cache.js"; export function createFsPromises(metadataCache, contentCache, transport) { return { @@ -45,6 +46,25 @@ export function createFsPromises(metadataCache, contentCache, transport) { const wantText = encoding === "utf8" || encoding === "utf-8"; + // Check input cache for files picked via browser file dialogs. + if (isInputCachePath(path)) { + const inputData = inputCacheGet(path); + + if (inputData !== null) { + if (wantText) { + return typeof inputData === "string" + ? inputData + : new TextDecoder().decode(inputData); + } + + if (typeof inputData === "string") { + return new TextEncoder().encode(inputData); + } + + return inputData; + } + } + const meta = metadataCache.get(path); if (meta && meta.type === "directory") { const e = new Error("EISDIR: illegal operation on a directory, read"); @@ -210,7 +230,9 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async open(path, flags) { - if (!metadataCache.has(path)) { + const hasInCache = isInputCachePath(path) && inputCacheGet(path) !== null; + + if (!hasInCache && !metadataCache.has(path)) { const err = new Error( `ENOENT: no such file or directory, open '${path}'`, ); diff --git a/src/shims/fs/sync.js b/src/shims/fs/sync.js index b499924..e102d3c 100644 --- a/src/shims/fs/sync.js +++ b/src/shims/fs/sync.js @@ -1,12 +1,31 @@ import { markLocalOp } from "./echo-guard.js"; +import { isInputCachePath, inputCacheGet } from "./input-cache.js"; export function createFsSync(metadataCache, contentCache, transport) { return { existsSync(path) { + if (isInputCachePath(path) && inputCacheGet(path) !== null) { + return true; + } + return metadataCache.has(path); }, statSync(path) { + if (isInputCachePath(path) && inputCacheGet(path) !== null) { + const data = inputCacheGet(path); + const size = data ? data.length || data.byteLength || 0 : 0; + + return { + size, + mtime: new Date(), + ctime: new Date(), + isFile: () => true, + isDirectory: () => false, + isSymbolicLink: () => false, + }; + } + const stat = metadataCache.toStat(path); if (!stat) { @@ -21,6 +40,10 @@ export function createFsSync(metadataCache, contentCache, transport) { }, accessSync(path, mode) { + if (isInputCachePath(path) && inputCacheGet(path) !== null) { + return; + } + if (!metadataCache.has(path)) { const err = new Error( `ENOENT: no such file or directory, access '${path}'`, @@ -42,6 +65,22 @@ export function createFsSync(metadataCache, contentCache, transport) { throw e; } + // Check input cache for files picked via browser file dialogs. + // These never hit the server; they exist only in browser memory. + if (isInputCachePath(path)) { + const inputData = inputCacheGet(path); + + if (inputData !== null) { + if (encoding === "utf8" || encoding === "utf-8") { + return typeof inputData === "string" + ? inputData + : new TextDecoder().decode(inputData); + } + + return inputData; + } + } + const cached = contentCache.get(path); if (cached !== null) { if (encoding === "utf8" || encoding === "utf-8") {