Filesystem primitives
Elide ships a host-internal filesystem layer in dev.elide.lang.javascript.fs.host.
It is not exposed to guest JavaScript directly — node:fs, whatwg/file, and
whatwg/formdata layer on top.
Public types
| Type | Purpose |
|---|---|
FsPrimitives | Sync POSIX-shaped op set over java.nio.file.* + FileChannel. |
FsPrimitivesAsync | Async siblings returning CompletableFuture on a dedicated filesystem-IO thread pool (IoPool). |
FsException | Runtime exception in Node's shape (code/errno/syscall/path/dest). |
FsErrno | Forward + reverse mapping between POSIX codes and platform errno (Linux / macOS / Windows). |
FsAccessFlag, FsOpenFlag, FsCopyFlag | Bit-flag constants for access / open / copyFile. |
FsStatInfo | Result of stat/lstat — Node's fs.Stats shape, nanosecond timestamps. |
FsDirEntry | One entry from readdir — name + type. |
FsWatchEvent | Watcher callback payload — Kind (CREATE/MODIFY/DELETE/RENAME/OVERFLOW) + Path. |
FsWatcher | Watch handle. AutoCloseable — close() stops the watch. |
Watchers | Backend registry + factory. |
FsBlobBackingStore | File-backed Blob: lazy openStream/readAllBytes/readAllText/slice. |
FsHostInit | Runtime hook that picks the right native watcher for the host OS. |
FsStatFsInfo | Result of statfs — filesystem-level statistics. |
FsPolicy | Sandbox policy that gates filesystem access (see the Filesystem Sandbox guide). |
Coverage
FsPrimitives exposes: open, close, read, write, stat, lstat, access,
chmod, chown, utimes, realpath, readdir, mkdir, rmdir, unlink, rename,
copyFile, symlink, readlink, truncate, ftruncate, fsync, fdatasync, statfs,
glob, link, mkdtemp, rm, cp, readFileBytes, writeFileBytes, readAllFromFd,
fstat, fchmod, fchown, futimes, lchmod, lchown, lutimes, mkdirRecursive,
and readdirRecursive — 41 operations in total. Each
has a sync entry on FsPrimitives and an async sibling on FsPrimitivesAsync that
returns a CompletableFuture and runs the work on a dedicated filesystem-IO thread pool (IoPool), not the common ForkJoinPool.
open returns a 64-bit handle (not a real fd) — calls thread it through subsequent ops
and release it via close. Internally each handle resolves to a FileChannel kept in
a ConcurrentHashMap; concurrent open from many threads runs without serialising.
Errno
Every error path materialises as FsException carrying:
code— the POSIX-style string Node surfaces (ENOENT,EACCES,EEXIST, …)errno— the numeric value on the originating platform (Linux's 2 vs macOS's 2 vs Windows' 2 — allENOENTbut the numeric mapping differs further along)syscall— the operation that failed (open,stat,rename, …)path— primary pathdest— destination for two-arg syscalls (rename,copyFile)
FsErrno.fromException classifies JDK NIO exceptions into POSIX codes; FsErrno.fromErrno
takes a raw platform errno and OS hint and returns the matching code. The Linux,
macOS, and Windows tables come from errno.h / sys/errno.h and libuv's mapping of
Win32 error codes onto POSIX names.
A few op-specific cases not derivable from JDK exceptions:
O_DIRECTORYon a non-directory target raisesENOTDIRsynchronously; on a directory it raisesEISDIRbecauseFileChannelcannot open a directory on the JDK.O_DIRECTis accepted but is a JVM no-op — the JDK has no portable hook for page-cache bypass. Callers that depend on the bypass must layer a syscall-level open on top.copyFile(..., COPYFILE_FICLONE_FORCE)raisesENOSYSbecause JDKFiles.copyhas no reflink hook and would silently degrade to a regular copy, which is the exact failure mode the flag is meant to surface.
Watchers
Watchers.open(path, recursive, listener) opens an FsWatcher. Four backends share
the same surface:
| Backend | OS | Latency | Recursive support |
|---|---|---|---|
InotifyWatcherBackend | Linux | Sub-second | Tree walk + per-subdir watches; new subdirs registered on the fly. |
FseventsWatcherBackend | macOS / Darwin | ~50 ms | Native (FSEvents reports the whole tree from one stream). |
RdcWatcherBackend | Windows | ~20 ms | Native (bWatchSubtree flag on ReadDirectoryChangesW). |
JdkWatchServiceBackend | Cross-platform fallback | OS-dependent (poll on macOS — 10 s) | Manual subtree register + on-the-fly. |
FsHostInit.install() runs at runtime startup (called from Entry.kt after
NativeLibs.loadAndInitialize). It picks the native backend matching the host OS;
if symbol probing fails, the JDK fallback stays in.
FsWatchEvent.OVERFLOW is dispatched when the OS reports an event-buffer overflow
(IN_Q_OVERFLOW on Linux, kFSEventStreamEventFlagMustScanSubDirs on macOS, RDC buffer
exhaustion on Windows). It is also dispatched when inotify_add_watch(2) fails for
any subtree directory (typically ENOSPC/EACCES) so the consumer knows that part
of the tree is un-monitored. Consumers should treat overflow as "events may have been
dropped" and re-stat the watched tree.
Rename pairs are matched per-cookie on Linux (IN_MOVED_FROM ↔ IN_MOVED_TO) and
in-buffer on Windows (FILE_ACTION_RENAMED_OLD_NAME immediately precedes
RENAMED_NEW_NAME); macOS rename events are surfaced individually because FSEvents
does not expose a stable pairing primitive. Unmatched IN_MOVED_FROM cookies older
than one second expire as plain DELETE events to bound memory growth — the bound
matters for long-running watchers on busy directories.
Recursive JDK fallback: after registering a freshly-created subdirectory, the backend
re-walks its contents and synthesises CREATE events. Children that landed during the
race window between mkdir and WatchService.register would otherwise be lost; the
extra walk closes that hole at the cost of (rare, idempotent) duplicate CREATEs.
Blob backing store
FsBlobBackingStore.of(path, type) produces a Blob view over the file at path. The
view lives without opening any file handle until the first read. Methods:
readAllBytes()— loads the entire byte range into a heap array. ThrowsEFBIGon views larger thanInteger.MAX_VALUErather than the JDK's defaultArithmeticException, so caller error handling stays in the POSIX-shaped lane.readAllText(encoding)— same, decoded as the named character set (default UTF-8).openStream()— freshInputStreampositioned at the view's offset, bounded by its size. The position is set viaFileChannel.position()before the stream is wrapped, so partial seeks (whichInputStream.skip()is permitted to return) cannot leave the reader at the wrong offset.slice(start, end, type)— sibling view; the slice does not open a separate file handle, it only re-clamps the byte range.
Used by fs.openAsBlob (T2.5) and node:fs.createReadStream (also T2.5).
Threading
FsPrimitives operations are individually thread-safe — every op opens its own
FileChannel (or looks up an open one by handle ID, then operates on the JDK channel
which is itself thread-safe for positional reads / writes). FsWatcher events are
delivered on the backend's pump thread, never on the calling thread; consumers that
need a single-threaded dispatch context (e.g. graal-js' realm thread) must marshal
the event there themselves.
Naming
The package is fs.host rather than the more obvious fs.native: native is a
Java reserved keyword and may not be used as a package identifier. host matches
the HostEnvironment naming used elsewhere for
host-side / sandbox-mediated capabilities.