minor refactor, cleanup

This commit is contained in:
Nystik
2026-03-11 22:08:30 +01:00
parent 2b9ebf1fbd
commit 9789be6d70
38 changed files with 259 additions and 379 deletions

View File

@@ -1,17 +1,10 @@
// Filesystem shim - the core piece
// Returned for both require('original-fs') and require('fs')
//
// Strategy: metadata cache + on-demand content fetch + write-through
// Server sync mechanism (REST vs WebSocket) is TBD - abstracted behind
// the transport layer in ./transport.js
import { MetadataCache } from './metadata-cache.js';
import { ContentCache } from './content-cache.js';
import { transport } from './transport.js';
import { createFsPromises } from './promises.js';
import { createFsSync } from './sync.js';
import { createFsWatch } from './watch.js';
import { constants } from './constants.js';
import { MetadataCache } from "./metadata-cache.js";
import { ContentCache } from "./content-cache.js";
import { transport } from "./transport.js";
import { createFsPromises } from "./promises.js";
import { createFsSync } from "./sync.js";
import { createFsWatch } from "./watch.js";
import { constants } from "./constants.js";
const metadataCache = new MetadataCache();
const contentCache = new ContentCache();
@@ -21,10 +14,8 @@ const fsSync = createFsSync(metadataCache, contentCache, transport);
const fsWatch = createFsWatch(transport);
export const fsShim = {
// Async promise-based API (this.fsPromises = this.fs.promises)
promises: fsPromises,
// Sync methods
existsSync: fsSync.existsSync,
readFileSync: fsSync.readFileSync,
writeFileSync: fsSync.writeFileSync,
@@ -33,17 +24,12 @@ export const fsShim = {
statSync: fsSync.statSync,
readdirSync: fsSync.readdirSync,
// Watch
watch: fsWatch.watch,
// Constants
constants,
// Internal: for initialization
_metadataCache: metadataCache,
_contentCache: contentCache,
// Initialize the caches by fetching the full tree from server
async _init(basePath) {
const tree = await transport.fetchTree(basePath);
metadataCache.populate(tree);

View File

@@ -1,10 +1,6 @@
// Async fs.promises implementation
// Maps to transport layer (REST/WebSocket/hybrid - TBD)
export function createFsPromises(metadataCache, contentCache, transport) {
return {
async stat(path) {
// Try cache first, fall back to server
const cached = metadataCache.toStat(path);
if (cached) return cached;
@@ -14,17 +10,15 @@ export function createFsPromises(metadataCache, contentCache, transport) {
},
async lstat(path) {
// No symlinks in our context - same as stat
// No symlinks in our context
return this.stat(path);
},
async readdir(path) {
// If metadata cache knows this is a file, return empty (ENOTDIR)
const meta = metadataCache.get(path);
if (meta && meta.type === "file") {
return [];
}
// If path not in cache at all (and not root), it doesn't exist
if (!meta && path && path !== "/" && path !== ".") {
const e = new Error(
`ENOENT: no such file or directory, scandir '${path}'`,
@@ -32,7 +26,6 @@ export function createFsPromises(metadataCache, contentCache, transport) {
e.code = "ENOENT";
throw e;
}
// Serve from metadata cache
const entries = metadataCache.readdir(path);
return entries.map((e) => e.name);
},
@@ -41,14 +34,12 @@ export function createFsPromises(metadataCache, contentCache, transport) {
if (typeof encoding === "object") encoding = encoding?.encoding;
const wantText = encoding === "utf8" || encoding === "utf-8";
// Short-circuit: reading a directory is an error
const meta = metadataCache.get(path);
if (meta && meta.type === "directory") {
const e = new Error("EISDIR: illegal operation on a directory, read");
e.code = "EISDIR";
throw e;
}
// Short-circuit: file not in metadata cache → doesn't exist
if (!meta && path) {
const e = new Error(
`ENOENT: no such file or directory, open '${path}'`,
@@ -57,7 +48,6 @@ export function createFsPromises(metadataCache, contentCache, transport) {
throw e;
}
// Check content cache
const cached = contentCache.get(path);
if (cached !== null) {
if (wantText) {
@@ -65,14 +55,13 @@ export function createFsPromises(metadataCache, contentCache, transport) {
? cached
: new TextDecoder().decode(cached);
}
// Binary mode: ensure we return a proper Uint8Array with .buffer
// binary. ensure we return a proper Uint8Array with .buffer
if (typeof cached === "string") {
return new TextEncoder().encode(cached);
}
return cached;
}
// Fetch from server
const data = await transport.readFile(path, encoding);
contentCache.set(path, data);
return data;
@@ -81,7 +70,6 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async writeFile(path, data, encoding) {
if (typeof encoding === "object") encoding = encoding?.encoding;
// Update caches optimistically
contentCache.set(path, data);
const size =
typeof data === "string" ? data.length : data.byteLength || 0;
@@ -92,9 +80,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
ctime: metadataCache.get(path)?.ctime || Date.now(),
});
// Send to server
const result = await transport.writeFile(path, data, encoding);
// Update metadata with server-confirmed values
if (result.mtime) {
metadataCache.set(path, {
type: "file",
@@ -108,7 +94,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async appendFile(path, data, encoding) {
contentCache.invalidate(path);
await transport.appendFile(path, data);
// Refresh metadata
const meta = await transport.stat(path);
metadataCache.set(path, meta);
},
@@ -120,13 +106,11 @@ export function createFsPromises(metadataCache, contentCache, transport) {
},
async rename(oldPath, newPath) {
// Move content cache entry
const content = contentCache.get(oldPath);
if (content !== null) {
contentCache.set(newPath, content);
contentCache.delete(oldPath);
}
// Move metadata
metadataCache.rename(oldPath, newPath);
await transport.rename(oldPath, newPath);
@@ -154,7 +138,6 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async copyFile(src, dest) {
await transport.copyFile(src, dest);
// Refresh metadata for dest
const meta = await transport.stat(dest);
metadataCache.set(dest, meta);
},
@@ -169,7 +152,6 @@ export function createFsPromises(metadataCache, contentCache, transport) {
},
async realpath(path) {
// Empty path = vault root, return the vault base path
if (!path || path === "/" || path === ".") return "/";
return transport.realpath(path);
},

View File

@@ -1,6 +1,3 @@
// Synchronous fs method implementations
// Served from caches where possible, sync XHR fallback for uncached content.
export function createFsSync(metadataCache, contentCache, transport) {
return {
existsSync(path) {
@@ -32,7 +29,6 @@ export function createFsSync(metadataCache, contentCache, transport) {
readFileSync(path, encoding) {
if (typeof encoding === "object") encoding = encoding?.encoding;
// Short-circuit: reading a directory is an error
const meta = metadataCache.get(path);
if (meta && meta.type === "directory") {
const e = new Error("EISDIR: illegal operation on a directory, read");
@@ -40,7 +36,6 @@ export function createFsSync(metadataCache, contentCache, transport) {
throw e;
}
// Try content cache first
const cached = contentCache.get(path);
if (cached !== null) {
if (encoding === "utf8" || encoding === "utf-8") {
@@ -51,7 +46,6 @@ export function createFsSync(metadataCache, contentCache, transport) {
return cached;
}
// Fallback: synchronous XHR
console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path);
const data = transport.readFileSync(path, encoding);
contentCache.set(path, data);
@@ -61,7 +55,6 @@ export function createFsSync(metadataCache, contentCache, transport) {
writeFileSync(path, data, encoding) {
if (typeof encoding === "object") encoding = encoding?.encoding;
// Write to cache immediately (sync return)
contentCache.set(path, data);
const size =
typeof data === "string" ? data.length : data.byteLength || 0;
@@ -86,7 +79,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
contentCache.delete(path);
metadataCache.delete(path);
// Fire-and-forget - suppress ENOENT (file already gone, e.g. .OBSIDIANTEST race)
// Fire-and-forget - suppress ENOENT (file already gone)
transport.unlink(path).catch((e) => {
if (e.code !== "ENOENT") {
console.error(

View File

@@ -1,16 +1,9 @@
// Transport abstraction layer
// Decouples the fs shim from the sync mechanism (REST, WebSocket, or hybrid).
// Currently implements a REST-based transport. This can be swapped or extended
// once the sync strategy is finalized.
const API_BASE = "/api/fs";
// Strip leading slashes from paths before sending to server
function normPath(p) {
return (p || "").replace(/^\/+/, "");
}
// Convert a Uint8Array to base64 without blowing the stack
function uint8ToBase64(bytes) {
let binary = "";
const chunk = 8192;
@@ -56,12 +49,10 @@ async function requestJson(method, endpoint, params = {}) {
return res.json();
}
// Synchronous XHR - used only as fallback for sync fs calls on uncached content.
// Blocking but functional. Should be rare after pre-warming.
function requestSync(method, endpoint, params = {}) {
const url = new URL(API_BASE + endpoint, window.location.origin);
if (method === "GET") {
if (method === "GET" || method === "DELETE") {
if (vaultId()) url.searchParams.set("vault", vaultId());
for (const [key, val] of Object.entries(params)) {
url.searchParams.set(key, val);
@@ -71,7 +62,7 @@ function requestSync(method, endpoint, params = {}) {
const xhr = new XMLHttpRequest();
xhr.open(method, url.toString(), false); // synchronous
if (method !== "GET") {
if (method !== "GET" && method !== "DELETE") {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ vault: vaultId(), ...params }));
} else {
@@ -95,8 +86,6 @@ function requestSync(method, endpoint, params = {}) {
}
export const transport = {
// --- Async methods (used by fs.promises) ---
async fetchTree(basePath) {
return requestJson("GET", "/tree", basePath ? { path: basePath } : {});
},
@@ -190,8 +179,6 @@ export const transport = {
});
},
// --- Sync methods (fallback) ---
readFileSync(path, encoding) {
const xhr = requestSync("GET", "/readFile", {
path: normPath(path),
@@ -200,7 +187,6 @@ export const transport = {
if (encoding === "utf8" || encoding === "utf-8") {
return xhr.responseText;
}
// Binary: return as Uint8Array
const binary = xhr.responseText;
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {

View File

@@ -1,14 +1,9 @@
// File watching shim
// Translates fs.watch() calls into WebSocket subscriptions.
// The server pushes file-change events; this module dispatches them
// to registered watch listeners.
export function createFsWatch(transport) {
const watchers = new Map(); // path -> Set<listener>
return {
watch(path, options, listener) {
if (typeof options === 'function') {
if (typeof options === "function") {
listener = options;
options = {};
}
@@ -32,22 +27,28 @@ export function createFsWatch(transport) {
}
}
},
on() { return this; },
once() { return this; },
removeListener() { return this; },
on() {
return this;
},
once() {
return this;
},
removeListener() {
return this;
},
};
},
// Internal: called when transport receives a file-change event
_dispatch(eventType, filePath) {
for (const [watchPath, listeners] of watchers) {
if (filePath === watchPath || filePath.startsWith(watchPath + '/')) {
if (filePath === watchPath || filePath.startsWith(watchPath + "/")) {
const relativeName = filePath.slice(watchPath.length + 1) || filePath;
for (const fn of listeners) {
try {
fn(eventType, relativeName);
} catch (e) {
console.error('[shim:fs:watch] Listener error:', e);
console.error("[shim:fs:watch] Listener error:", e);
}
}
}