mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
improve sync input workaround
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}'`,
|
||||
);
|
||||
|
||||
123
src/shims/fs/input-cache.js
Normal file
123
src/shims/fs/input-cache.js
Normal file
@@ -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/");
|
||||
}
|
||||
@@ -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}'`,
|
||||
);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user