Skip to content

Fileserver

Defined in: packages/core/src/kernel/types.ts:528

universal filesystem protocol — any object implementing these methods is a fileserver.

Implement this to mount arbitrary backing storage (memory, overlay, HTTP, proc, etc.) into the namespace. The kernel routes all I/O through this protocol.

The kernel routes all I/O through fileservers via namespace resolution. A fileserver never sees process-level fd numbers — open() returns an opaque token that the kernel maps to a process fd. All data on the wire is Uint8Array.

Fileservers are composable: an overlay fileserver layers two fileservers for copy-on-write, union bind stacks multiple fileservers at one mount point. The protocol is small enough that fundamental capabilities (overlay, readonly, proxy) are just more fileservers — see readonlyFS for the simplest example.

Paths arriving at any method are InnerPath — mount-relative, already normalized by the kernel (no ., .., double slashes, or trailing /).

The fd argument to read / write / close is always a token that this same server returned from a prior open() — never a kernel fd number and never a token from a different server.

Errors are reported by throwing an FsError with the appropriate FsErrorCode. Returning undefined or a sentinel value is not a protocol-conformant way to signal failure.

checkAccess is opt-in. When present, the kernel calls it INSTEAD of the default mode-bit check; when absent, the kernel falls back to checking mode / uid / gid from stat(). A server that implements neither has no access control.

import { memoryFS, type Fileserver } from '@fishnet/core'
// A minimal fileserver that wraps memoryFS() and counts method calls.
// Demonstrates the "composability" property — a Fileserver can be any object
// satisfying the protocol, not just one produced by a built-in factory.
function countingFS(): Fileserver & { calls: ReadonlyMap<string, number> } {
const inner = memoryFS()
const calls = new Map<string, number>()
const track = <T extends unknown[], R>(name: string, fn: (...args: T) => R) =>
(...args: T): R => {
calls.set(name, (calls.get(name) ?? 0) + 1)
return fn(...args)
}
return {
type: 'counting',
open: track('open', inner.open.bind(inner)),
read: track('read', inner.read.bind(inner)),
write: track('write', inner.write.bind(inner)),
close: track('close', inner.close.bind(inner)),
stat: track('stat', inner.stat.bind(inner)),
readdir: track('readdir', inner.readdir.bind(inner)),
mkdir: track('mkdir', inner.mkdir.bind(inner)),
remove: track('remove', inner.remove.bind(inner)),
rename: track('rename', inner.rename.bind(inner)),
wstat: track('wstat', inner.wstat.bind(inner)),
calls,
}
}

readonly optional type?: string

Defined in: packages/core/src/kernel/types.ts:530

Optional human-readable type tag (e.g. ‘memory’, ‘overlay’, ‘proc’).

optional checkAccess(path, flags, caller): Promise<void>

Defined in: packages/core/src/kernel/types.ts:642

Optional permission override. When present, kernel calls this INSTEAD of the default mode-bit check. Allows synthetic fileservers (procFS, devFS, etc.) to implement custom access control semantics.

InnerPath

OpenFlags

Gid

Uid

Promise<void>

Throw EACCES / EPERM to deny; resolve void to grant. The return value of a successful check is ignored — only the throw path matters

Called BEFORE open() on every access — the server must not rely on open() to re-check permissions


close(fd): Promise<void>

Defined in: packages/core/src/kernel/types.ts:572

Close an open fd token.

unknown

Promise<void>


mkdir(path): Promise<void>

Defined in: packages/core/src/kernel/types.ts:602

Create a directory. Throws EEXIST if it already exists.

InnerPath

Promise<void>

Parent directory must exist — the server does not perform mkdir -p; throw ENOENT when the parent is missing


open(path, flags, pid?): Promise<unknown>

Defined in: packages/core/src/kernel/types.ts:544

Open a file or directory, returning an opaque fd token.

InnerPath

OpenFlags

Pid

Promise<unknown>

path is mount-relative (InnerPath) — the server never sees absolute paths or .. / . segments

The returned token is opaque to the kernel; it MUST round-trip through this same server’s read / write / close

Throws ENOENT when the path does not exist and flags.create is not set — the kernel does not pre-check existence

pid is informational only (for audit / permission scoping); the server MUST NOT use it as a routing key or trust it for authorization


read(fd, offset, count): Promise<Uint8Array<ArrayBufferLike>>

Defined in: packages/core/src/kernel/types.ts:558

Read up to count bytes from an open fd at the given offset.

unknown

number

number

Promise<Uint8Array<ArrayBufferLike>>

offset is absolute within the file — the kernel tracks the per-fd cursor and passes the resolved offset on each call; the server is stateless with respect to position

Return length ≤ count; a short read (including length 0) is how the server signals EOF — it is NOT an error condition

The returned Uint8Array may be a view into the server’s own backing storage; callers MUST NOT mutate the buffer or retain it past the next call on the same fd


readdir(path): Promise<DirEntry[]>

Defined in: packages/core/src/kernel/types.ts:594

List entries in a directory. Throws ENOTDIR if path is a file.

InnerPath

Promise<DirEntry[]>

Entry names are basenames, not full paths — the caller joins them with path when needed

. and .. are NOT included; the kernel synthesizes parent / self traversal from the namespace, not the server


remove(path): Promise<void>

Defined in: packages/core/src/kernel/types.ts:610

Remove a file or empty directory.

InnerPath

Promise<void>

Throws ENOTEMPTY when removing a non-empty directory — recursive removal is a caller-level concern, not a server responsibility


rename(oldPath, newPath): Promise<void>

Defined in: packages/core/src/kernel/types.ts:620

Rename/move a file or directory.

InnerPath

InnerPath

Promise<void>

Both oldPath and newPath resolve to THIS server — cross-mount renames are rejected by the kernel with EXDEV before reaching here

Rename is atomic from the kernel’s perspective: either both paths are consistent on return, or the call throws and nothing changed


stat(path, opts?): Promise<Stat>

Defined in: packages/core/src/kernel/types.ts:584

Return metadata for a path. Throws ENOENT if not found. When opts.nofollow is true, return the symlink node itself (lstat behavior).

InnerPath

boolean

Promise<Stat>

stat is how the kernel implements the default mode-bit access check — servers that want to hide nodes from non-privileged callers should implement checkAccess rather than lying in stat

Stat.size is bytes for files, implementation-defined for directories and streams (commonly 0); do not rely on it for stream-like nodes


write(fd, offset, data): Promise<number>

Defined in: packages/core/src/kernel/types.ts:569

Write data at the given offset, returning bytes written.

unknown

number

Uint8Array

Promise<number>

Return value is bytes actually written; a short write is legal and the kernel will retry from offset + written — do not throw on partial writes

The server MUST NOT retain data past the returned promise — the kernel may reuse the buffer once the promise settles

Throws EPERM on a read-only server rather than silently dropping data


wstat(path, changes): Promise<void>

Defined in: packages/core/src/kernel/types.ts:630

Update metadata fields (mode, uid, gid, mtime, size).

InnerPath

Partial<StatChanges>

Promise<void>

Only fields present in changes are modified — absent fields are preserved; passing {} is a valid no-op

size changes implement truncate / extend semantics on file content; extending beyond current size zero-fills