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

TypePurpose
FsPrimitivesSync POSIX-shaped op set over java.nio.file.* + FileChannel.
FsPrimitivesAsyncAsync siblings returning CompletableFuture on a dedicated filesystem-IO thread pool (IoPool).
FsExceptionRuntime exception in Node's shape (code/errno/syscall/path/dest).
FsErrnoForward + reverse mapping between POSIX codes and platform errno (Linux / macOS / Windows).
FsAccessFlag, FsOpenFlag, FsCopyFlagBit-flag constants for access / open / copyFile.
FsStatInfoResult of stat/lstat — Node's fs.Stats shape, nanosecond timestamps.
FsDirEntryOne entry from readdir — name + type.
FsWatchEventWatcher callback payload — Kind (CREATE/MODIFY/DELETE/RENAME/OVERFLOW) + Path.
FsWatcherWatch handle. AutoCloseableclose() stops the watch.
WatchersBackend registry + factory.
FsBlobBackingStoreFile-backed Blob: lazy openStream/readAllBytes/readAllText/slice.
FsHostInitRuntime hook that picks the right native watcher for the host OS.
FsStatFsInfoResult of statfs — filesystem-level statistics.
FsPolicySandbox 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 — all ENOENT but the numeric mapping differs further along)
  • syscall — the operation that failed (open, stat, rename, …)
  • path — primary path
  • dest — 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_DIRECTORY on a non-directory target raises ENOTDIR synchronously; on a directory it raises EISDIR because FileChannel cannot open a directory on the JDK.
  • O_DIRECT is 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) raises ENOSYS because JDK Files.copy has 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:
BackendOSLatencyRecursive support
InotifyWatcherBackendLinuxSub-secondTree walk + per-subdir watches; new subdirs registered on the fly.
FseventsWatcherBackendmacOS / Darwin~50 msNative (FSEvents reports the whole tree from one stream).
RdcWatcherBackendWindows~20 msNative (bWatchSubtree flag on ReadDirectoryChangesW).
JdkWatchServiceBackendCross-platform fallbackOS-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_FROMIN_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. Throws EFBIG on views larger than Integer.MAX_VALUE rather than the JDK's default ArithmeticException, so caller error handling stays in the POSIX-shaped lane.
  • readAllText(encoding) — same, decoded as the named character set (default UTF-8).
  • openStream() — fresh InputStream positioned at the view's offset, bounded by its size. The position is set via FileChannel.position() before the stream is wrapped, so partial seeks (which InputStream.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.