improve sync input workaround
This commit is contained in:
parent
f14cac6490
commit
4a4d904420
5 changed files with 215 additions and 13 deletions
|
|
@ -3,7 +3,7 @@ import {
|
||||||
showConfirmDialog,
|
showConfirmDialog,
|
||||||
showPromptDialog,
|
showPromptDialog,
|
||||||
} from "../../../ui/bootstrap.js";
|
} from "../../../ui/bootstrap.js";
|
||||||
import { transport } from "../../fs/transport.js";
|
import { inputCacheSet, inputCacheDelete } from "../../fs/input-cache.js";
|
||||||
|
|
||||||
const IMPORTS_DIR = ".obsidian/imports";
|
const IMPORTS_DIR = ".obsidian/imports";
|
||||||
const STAGED_TTL_MS = 120_000; // 2 minutes
|
const STAGED_TTL_MS = 120_000; // 2 minutes
|
||||||
|
|
@ -24,7 +24,7 @@ function clearStagedFiles() {
|
||||||
console.log("[shim:dialog] Clearing expired staged files");
|
console.log("[shim:dialog] Clearing expired staged files");
|
||||||
|
|
||||||
for (const p of staged.paths) {
|
for (const p of staged.paths) {
|
||||||
transport.unlink(p.replace(/^\//, "")).catch(() => {});
|
inputCacheDelete(p.replace(/^\//, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
staged = { paths: [], fingerprint: null, timestamp: 0 };
|
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 arrayBuffer = await file.arrayBuffer();
|
||||||
const bytes = new Uint8Array(arrayBuffer);
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
const targetPath = IMPORTS_DIR + "/" + file.name;
|
const targetPath = IMPORTS_DIR + "/" + file.name;
|
||||||
|
|
||||||
await transport.writeFile(targetPath, bytes);
|
inputCacheSet(targetPath, bytes);
|
||||||
|
|
||||||
return "/" + targetPath;
|
return "/" + targetPath;
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ async function startWorkaroundFlow(options, fingerprint) {
|
||||||
const paths = [];
|
const paths = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const vaultPath = await uploadToImports(file);
|
const vaultPath = await cacheToImports(file);
|
||||||
paths.push(vaultPath);
|
paths.push(vaultPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ async function startWorkaroundFlow(options, fingerprint) {
|
||||||
|
|
||||||
await showMessageDialog(
|
await showMessageDialog(
|
||||||
"Files Ready",
|
"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.",
|
"The files will be provided automatically.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -134,11 +134,11 @@ export const dialogShim = {
|
||||||
const filePaths = [];
|
const filePaths = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const vaultPath = await uploadToImports(file);
|
const vaultPath = await cacheToImports(file);
|
||||||
filePaths.push(vaultPath);
|
filePaths.push(vaultPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[shim:dialog] showOpenDialog - uploaded:", filePaths);
|
console.log("[shim:dialog] showOpenDialog - cached:", filePaths);
|
||||||
return { canceled: false, filePaths };
|
return { canceled: false, filePaths };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -187,9 +187,10 @@ export const dialogShim = {
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
"Feature Not Available",
|
"Feature Not Available",
|
||||||
"This action requires a native file picker which is not available in the browser.",
|
"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. " +
|
"A workaround is available: select your files first, then retry the action. " +
|
||||||
"Would you like to proceed?",
|
"They will be provided automatically.\n\n" +
|
||||||
"Upload File",
|
"Note: individual files must be under 200 MB.",
|
||||||
|
"Select Files",
|
||||||
).then((confirmed) => {
|
).then((confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
startWorkaroundFlow(options, callerFingerprint);
|
startWorkaroundFlow(options, callerFingerprint);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,26 @@
|
||||||
// Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek
|
// Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek
|
||||||
// around files without loading them via readFileSync upfront.
|
// around files without loading them via readFileSync upfront.
|
||||||
|
|
||||||
|
import { isInputCachePath, inputCacheGet } from "./input-cache.js";
|
||||||
|
|
||||||
let nextFd = 100;
|
let nextFd = 100;
|
||||||
const openFiles = new Map();
|
const openFiles = new Map();
|
||||||
|
|
||||||
export function createFdOps(metadataCache, contentCache, transport) {
|
export function createFdOps(metadataCache, contentCache, transport) {
|
||||||
function ensureData(path) {
|
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);
|
const cached = contentCache.get(path);
|
||||||
|
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
|
|
@ -40,7 +55,9 @@ export function createFdOps(metadataCache, contentCache, transport) {
|
||||||
// --- Sync ---
|
// --- Sync ---
|
||||||
|
|
||||||
function openSync(path, flags, mode) {
|
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(
|
const err = new Error(
|
||||||
`ENOENT: no such file or directory, open '${path}'`,
|
`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 { markLocalOp } from "./echo-guard.js";
|
||||||
|
import { isInputCachePath, inputCacheGet } from "./input-cache.js";
|
||||||
|
|
||||||
export function createFsPromises(metadataCache, contentCache, transport) {
|
export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -45,6 +46,25 @@ export function createFsPromises(metadataCache, contentCache, transport) {
|
||||||
|
|
||||||
const wantText = encoding === "utf8" || encoding === "utf-8";
|
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);
|
const meta = metadataCache.get(path);
|
||||||
if (meta && meta.type === "directory") {
|
if (meta && meta.type === "directory") {
|
||||||
const e = new Error("EISDIR: illegal operation on a directory, read");
|
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) {
|
async open(path, flags) {
|
||||||
if (!metadataCache.has(path)) {
|
const hasInCache = isInputCachePath(path) && inputCacheGet(path) !== null;
|
||||||
|
|
||||||
|
if (!hasInCache && !metadataCache.has(path)) {
|
||||||
const err = new Error(
|
const err = new Error(
|
||||||
`ENOENT: no such file or directory, open '${path}'`,
|
`ENOENT: no such file or directory, open '${path}'`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,31 @@
|
||||||
import { markLocalOp } from "./echo-guard.js";
|
import { markLocalOp } from "./echo-guard.js";
|
||||||
|
import { isInputCachePath, inputCacheGet } from "./input-cache.js";
|
||||||
|
|
||||||
export function createFsSync(metadataCache, contentCache, transport) {
|
export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
return {
|
return {
|
||||||
existsSync(path) {
|
existsSync(path) {
|
||||||
|
if (isInputCachePath(path) && inputCacheGet(path) !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return metadataCache.has(path);
|
return metadataCache.has(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
statSync(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);
|
const stat = metadataCache.toStat(path);
|
||||||
|
|
||||||
if (!stat) {
|
if (!stat) {
|
||||||
|
|
@ -21,6 +40,10 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
},
|
},
|
||||||
|
|
||||||
accessSync(path, mode) {
|
accessSync(path, mode) {
|
||||||
|
if (isInputCachePath(path) && inputCacheGet(path) !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!metadataCache.has(path)) {
|
if (!metadataCache.has(path)) {
|
||||||
const err = new Error(
|
const err = new Error(
|
||||||
`ENOENT: no such file or directory, access '${path}'`,
|
`ENOENT: no such file or directory, access '${path}'`,
|
||||||
|
|
@ -42,6 +65,22 @@ export function createFsSync(metadataCache, contentCache, transport) {
|
||||||
throw e;
|
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);
|
const cached = contentCache.get(path);
|
||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
if (encoding === "utf8" || encoding === "utf-8") {
|
if (encoding === "utf8" || encoding === "utf-8") {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue