IO
A single effect-native IO capability — filesystem, process, crypto, paths and networking — with swappable runtime backends.
@ozaco/std/ioIO is a plugin protocol (defineProtocol) describing every side-effecting capability the standard library needs: reading and writing files, spawning processes, hashing and signing, joining paths, and opening TCP/UDP sockets. The protocol carries only the shape; a concrete runtime implementation must be installed before its actions run.
Pick a backend by import — BunIO, NodeIO or WebIO — and register it once with install(...) from std:plugin. From then on every action is reached through IO.actions.* and consumed inside an effect operation with yield*. Almost every action returns a Future<T> (an effect that resolves to a value); the streaming ones (readStream, writeStream, watch, fromReadable) return an effect Stream.
Paths accept a PathLike — a string or a file: URL — normalized through toPath. Because the surface is one protocol, code written against IO runs unchanged on Bun, Node or the browser; the web backend simply fails clearly for capabilities a browser cannot provide (filesystem, processes, sockets).
@ozaco/std/io/impl/bun`BunIO` — the Bun runtime backend. Uses `Bun.file`/`Bun.write` for fast file IO and Bun-native process spawning, with Web Crypto for hashing and random bytes.@ozaco/std/io/impl/node`NodeIO` — the Node.js backend. Built on `node:fs/promises`, `node:crypto` and `node:child_process`; the portable default for any Node deployment.@ozaco/std/io/impl/web`WebIO` — the browser backend. Provides crypto (Web Crypto) and a best-effort `env`; filesystem, process and socket actions fail with an `io-unsupported` failure instead of pretending to work.
Protocol
The capability protocol and its flag set.
IO
The IO capability protocol. Reach its actions via `IO.actions.*`; install a backend first.
const IO: Protocol<AnyType, [], IOActions>Created with defineProtocol({ name: 'io', version: '0.0.1' }). A runtime backend (BunIO/NodeIO/WebIO) is built from it with IO.implement(meta).build(actions) and registered with install(...). Consumers never touch a backend directly — they call IO.actions.readText(...) and the installed implementation answers.
import { install } from 'std:plugin'
import { IO } from '@ozaco/std/io'
import { BunIO } from '@ozaco/std/io/impl/bun'
yield* install(BunIO)
const text = yield* IO.actions.readText('config.json')
const id = yield* IO.actions.uuid()IO_FLAGS
Bit flags for write/copy/rename/walk behavior, combined with `|`.
enum IO_FLAGS {
NONE = 0,
FOLLOW_SYMLINKS = 1 << 2,
FILES = 1 << 3,
DIRS = 1 << 4,
APPEND = 1 << 5,
EXCLUSIVE = 1 << 6,
}APPEND and EXCLUSIVE shape write/copy/rename (exclusive fails if the destination exists); FILES/DIRS/FOLLOW_SYMLINKS filter walk. Combine them with a bitwise OR, e.g. IO_FLAGS.FILES | IO_FLAGS.DIRS.
import { IO, IO_FLAGS } from '@ozaco/std/io'
// append instead of truncate
yield* IO.actions.write('log.txt', 'line\n', { flags: IO_FLAGS.APPEND })
// walk files only
const files = yield* IO.actions.walk('src', { flags: IO_FLAGS.FILES })Filesystem
Read, write and manage files and directories. Every action takes a `PathLike`.
read
Read a whole file as raw bytes.
IO.actions.read(path: PathLike): Future<Uint8Array>const bytes = yield* IO.actions.read('logo.png')readText
Read a whole file as text (defaults to UTF-8).
IO.actions.readText(path: PathLike, encoding?: string): Future<string>const source = yield* IO.actions.readText('main.ts')
const latin = yield* IO.actions.readText('legacy.txt', 'latin1')write
Write bytes or text to a file, truncating unless `APPEND`/`EXCLUSIVE` is set.
IO.actions.write(path: PathLike, data: Uint8Array | string, options?: { flags?: number }): Future<void>yield* IO.actions.write('out.json', JSON.stringify(data))append
Append bytes to a file, creating it if missing.
IO.actions.append(path: PathLike, data: Uint8Array): Future<void>copy
Copy a file; with `EXCLUSIVE` fails if the destination exists.
IO.actions.copy(src: PathLike, dest: PathLike, options?: { flags?: number }): Future<void>rename
Move or rename a file; with `EXCLUSIVE` fails if the destination exists.
IO.actions.rename(src: PathLike, dest: PathLike, options?: { flags?: number }): Future<void>rm
Remove a file or directory, optionally recursively and forcefully.
IO.actions.rm(path: PathLike, options?: { recursive?: boolean; force?: boolean }): Future<void>yield* IO.actions.rm('dist', { recursive: true, force: true })exists
Whether a path exists (file or directory).
IO.actions.exists(path: PathLike): Future<boolean>if (yield* IO.actions.exists('.env')) {
// ...
}stat
File metadata, following symlinks. Returns an `IOStat`.
IO.actions.stat(path: PathLike): Future<IOStat>lstat
Like `stat`, but does not follow symlinks.
IO.actions.lstat(path: PathLike): Future<IOStat>readdir
List directory entries, optionally recursively.
IO.actions.readdir(path: PathLike, options?: { recursive?: boolean }): Future<string[]>ensureDir
Create a directory (and parents) if it does not already exist.
IO.actions.ensureDir(path: PathLike): Future<void>ensureFile
Create an empty file (and parent directories) if it does not already exist.
IO.actions.ensureFile(path: PathLike): Future<void>emptyDir
Ensure a directory exists and is empty, removing any existing contents.
IO.actions.emptyDir(path: PathLike): Future<void>walk
Recursively collect directory entries, filtered by flags, depth and regex match/skip lists.
IO.actions.walk(root: PathLike, options?: WalkOptions): Future<WalkEntry[]>const entries = yield* IO.actions.walk('src', {
flags: IO_FLAGS.FILES,
match: [/\.ts$/],
skip: [/node_modules/],
})watch
Watch a file or directory, streaming `WatchEvent`s until torn down.
IO.actions.watch(path: PathLike, options?: WatchOptions): Stream<WatchEvent, never>Event-based (backed by fsPromises.watch), recursive-capable. The stream never settles on its own — it runs until the surrounding scope tears it down.
chmod
Change a path’s permission mode.
IO.actions.chmod(path: PathLike, mode: number): Future<void>symlink
Create a symbolic link at `path` pointing to `target`.
IO.actions.symlink(target: PathLike, path: PathLike, type?: 'file' | 'dir' | 'junction'): Future<void>readlink
Read the target of a symbolic link.
IO.actions.readlink(path: PathLike): Future<string>Streams
Byte streams and adapters between effect `Stream`s and platform readables.
readStream
Open a file as a byte `Stream` (read lazily, chunk by chunk).
IO.actions.readStream(path: PathLike): Stream<Uint8Array, StreamClose>writeStream
Drain a byte `Stream` into a file.
IO.actions.writeStream(path: PathLike, source: Stream<Uint8Array, unknown>, options?: { flags?: number }): Future<void>fromReadable
Adapt a Node or Web readable into an effect byte `Stream`.
IO.actions.fromReadable(target: ReadableLike, options?: { destroy?: boolean }): Stream<Uint8Array, StreamClose>Works in every backend (including the browser) — the one stream action WebIO supports, for wrapping an existing web ReadableStream.
toReadable
Turn an effect byte `Stream` into a Web `ReadableStream` plus a `pump` operation to drive it.
IO.actions.toReadable(source: Stream<Uint8Array, unknown>): Future<{ readable: ReadableStream<Uint8Array>; pump: Operation<void> }>Process & env
Read environment variables and run child processes.
env
Read + shape environment variables through a mapper, asserting required keys are present.
IO.actions.env<R, K extends keyof R = never>(
mapper: (data: Record<string, string | undefined>) => R,
optional?: readonly K[],
): Future<{ [P in keyof R]: P extends K ? R[P] : NonNullable<R[P]> }>Keys not listed in optional are asserted non-nullable in the result type. WebIO reads a best-effort source.
const ENV = IO.actions.env(
data => ({ port: data.PORT, level: data.LOG_LEVEL }),
['level'],
)
const env = yield* ENV //=> { port: string; level: string | undefined }exec
Run a command to completion and buffer its output. Returns an `ExecResult`.
IO.actions.exec(cmd: string, args?: readonly string[], options?: ExecOptions): Future<ExecResult>const { stdout, success } = yield* IO.actions.exec('git', ['rev-parse', 'HEAD'])spawn
Spawn a child process and return a live `ProcessHandle` (streamed stdout/stderr, stdin, kill).
IO.actions.spawn(cmd: string, args?: readonly string[], options?: SpawnOptions): Future<ProcessHandle>Networking
TCP and UDP sockets, plus interface discovery.
tcpListen
Start a TCP server; `onConnection` runs per accepted socket as a child of the caller’s scope.
IO.actions.tcpListen(options: TcpListenOptions, onConnection: TcpHandler): Future<TcpServer>tcpConnect
Open a client TCP connection, returning a bidirectional `TcpSocket`.
IO.actions.tcpConnect(options: TcpConnectOptions): Future<TcpSocket>udpBind
Bind a UDP socket for send/receive of datagrams.
IO.actions.udpBind(options?: UdpBindOptions): Future<UdpSocket>ip
List the machine’s network interface addresses.
IO.actions.ip(): Future<NetworkInterface[]>Crypto & IDs
Random bytes, sortable/unique identifiers, hashing, symmetric encryption and Ed25519 signing.
randomBytes
Generate cryptographically strong random bytes.
IO.actions.randomBytes(length: number): Future<Uint8Array>ulid
Generate a ULID — lexicographically sortable and monotonic within a time `window`.
IO.actions.ulid(options?: UlidOptions): Future<string>const id = yield* IO.actions.ulid({ bucket: 'msg_' })uuid
Generate an RFC 4122 version-4 (random) UUID string.
IO.actions.uuid(): Future<string>const session = yield* IO.actions.uuid()hash
Hash bytes with SHA-256/384/512.
IO.actions.hash(algorithm: HashAlgorithm, data: Uint8Array): Future<Uint8Array>hmac
Compute an HMAC over bytes with a key and SHA-256/384/512.
IO.actions.hmac(algorithm: HashAlgorithm, key: Uint8Array, data: Uint8Array): Future<Uint8Array>encrypt
Encrypt with a secret (AES-256-GCM, key derived via scrypt). Reversible via `decrypt`.
IO.actions.encrypt(data: Uint8Array | string, secret: string): Future<Uint8Array>Not available in WebIO.
decrypt
Decrypt what `encrypt` produced; fails on a wrong secret or tampered data.
IO.actions.decrypt(data: Uint8Array, secret: string): Future<Uint8Array>generateKeyPair
Generate an Ed25519 key pair for `sign`/`verify`.
IO.actions.generateKeyPair(): Future<KeyPair>sign
Sign data with an Ed25519 private key; returns a 64-byte signature.
IO.actions.sign(data: Uint8Array | string, privateKey: Uint8Array): Future<Uint8Array>verify
Verify an Ed25519 signature against a public key.
IO.actions.verify(data: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array): Future<boolean>Paths
Path composition and inspection. The path helpers return a `Future`; `toPath` is a plain sync export.
join
Join path segments with the platform separator and normalize.
IO.actions.join(...segments: string[]): Future<string>const target = yield* IO.actions.join(dir, 'config', 'app.json')dirname
The directory portion of a path.
IO.actions.dirname(path: string): Future<string>basename
The final portion of a path; strips a trailing `suffix` when it matches.
IO.actions.basename(path: string, suffix?: string): Future<string>extname
The extension (including the leading dot), or `''` when there is none.
IO.actions.extname(path: string): Future<string>isAbsolute
Whether the path is absolute.
IO.actions.isAbsolute(path: string): Future<boolean>toPath
Normalize a `PathLike` (string or `file:` URL) into a plain filesystem path string.
function toPath(pathOrUrl: PathLike): stringA synchronous, non-effect helper. file:// URLs are decoded to their path; plain strings pass through unchanged.
import { toPath } from '@ozaco/std/io'
toPath('file:///etc/hosts') //=> '/etc/hosts'
toPath('relative/file.txt') //=> 'relative/file.txt'Types
The action contract and the value shapes actions return.
IOActions
The full record of IO actions the protocol declares (the shape behind `IO.actions`).
type IOActions = { read: ...; write: ...; exec: ...; /* every action */ }PathLike
A path argument: a string or a `URL`.
type PathLike = string | URLHashAlgorithm
Supported digest algorithms for `hash`/`hmac`.
type HashAlgorithm = 'SHA-256' | 'SHA-384' | 'SHA-512'StreamClose
The value a byte stream settles with: `true` on a clean end, or the failure that interrupted it.
type StreamClose = true | Result.Failure<unknown>IOStat
File metadata from `stat`/`lstat`.
interface IOStat {
isFile: boolean
isDirectory: boolean
isSymlink: boolean
size: number
mtime: Date | null
atime: Date | null
birthtime: Date | null
}WalkEntry
A single entry produced by `walk`.
interface WalkEntry {
path: string
name: string
isFile: boolean
isDirectory: boolean
isSymlink: boolean
}WatchEvent
A filesystem change reported by `watch`.
interface WatchEvent {
type: 'rename' | 'change'
path: string | null
}ProcessStatus
The exit status of a child process.
interface ProcessStatus {
code: number | null
signal: string | null
success: boolean
}ExecResult
The buffered result of running a command to completion via `exec`.
interface ExecResult extends ProcessStatus {
stdout: Uint8Array
stderr: Uint8Array
}ProcessHandle
A live handle to a spawned child process.
interface ProcessHandle {
readonly pid: number
readonly stdout: Stream<Uint8Array, StreamClose>
readonly stderr: Stream<Uint8Array, StreamClose>
exited: () => Future<ProcessStatus>
write: (chunk: Uint8Array | string) => Future<void>
closeStdin: () => Future<void>
kill: (signal?: number | string) => Future<void>
}KeyPair
An Ed25519 key pair (public = SPKI, private = PKCS8 DER bytes).
interface KeyPair {
publicKey: Uint8Array
privateKey: Uint8Array
}NetworkInterface
A single network interface address reported by `ip`.
interface NetworkInterface {
name: string
address: string
family: 'IPv4' | 'IPv6'
internal: boolean
mac: string
netmask: string
cidr: string | null
}TcpSocket
A bidirectional TCP byte channel (accepted connection or client socket).
interface TcpSocket {
readonly remoteAddress: string
readonly remotePort: number
readonly localPort: number
data: Stream<Uint8Array, StreamClose>
write: (chunk: Uint8Array | string) => Future<void>
close: () => Future<void>
}TcpHandler
A per-connection handler passed to `tcpListen`.
type TcpHandler = (socket: TcpSocket) => Operation<void>TcpServer
A handle to a listening TCP server.
interface TcpServer {
readonly port: number
readonly hostname: string
close: () => Future<void>
}UdpDatagram
A single received UDP datagram plus its sender.
interface UdpDatagram {
data: Uint8Array
address: string
port: number
}UdpSocket
A handle to a bound UDP socket.
interface UdpSocket {
readonly port: number
messages: Stream<UdpDatagram, StreamClose>
send: (data: Uint8Array | string, port: number, address: string) => Future<void>
close: () => Future<void>
}ReadableLike
A Node or Web readable accepted by `fromReadable`.
type ReadableLike = NodeReadableLike | WebReadableLikeNodeReadableLike
The event-emitter readable shape (`on`/`off`/`destroy?`).
interface NodeReadableLike {
on(event: string, listener: (...args: AnyType[]) => void): this
off(event: string, listener: (...args: AnyType[]) => void): this
destroy?(error?: Error): this
}WebReadableLike
The Web `ReadableStreamDefaultReader` shape (`read`/`cancel`/`releaseLock`).
interface WebReadableLike {
read(): Promise<ReadableStreamReadResult<Uint8Array>>
cancel(reason?: AnyType): Promise<void>
releaseLock(): void
}WritableLike
A writable sink shape (`write`/`end`/`on`/`once`/`off`).
interface WritableLike {
write(chunk: Uint8Array): boolean
end(): this
destroy?(error?: Error): this
on(event: string, listener: (...args: AnyType[]) => void): this
once(event: string, listener: (...args: AnyType[]) => void): this
off(event: string, listener: (...args: AnyType[]) => void): this
}Options
Option bags accepted by the actions above.
WalkOptions
Filters for `walk`: flags, depth cap and regex match/skip lists.
interface WalkOptions {
flags?: number
maxDepth?: number
match?: RegExp[]
skip?: RegExp[]
}WatchOptions
Options for `watch` — recurse into nested directories (default `false`).
interface WatchOptions {
recursive?: boolean
}UlidOptions
Shape a generated ULID: quantize the timestamp, set total length, or add a bucket prefix.
interface UlidOptions {
window?: number
length?: number
bucket?: string
}ProcessOptions
Base child-process options: working directory and environment overrides.
interface ProcessOptions {
cwd?: PathLike
env?: Record<string, string | undefined>
}ExecOptions
Options for `exec`: adds buffered `stdin` and a kill `timeout`.
interface ExecOptions extends ProcessOptions {
stdin?: Uint8Array | string
timeout?: number
}SpawnOptions
Options for `spawn` (an alias of `ProcessOptions`).
type SpawnOptions = ProcessOptionsTcpListenOptions
Options for `tcpListen`: port, bind hostname, and `SO_REUSEPORT`.
interface TcpListenOptions {
port: number
hostname?: string
reusePort?: boolean
}TcpConnectOptions
Options for `tcpConnect`: destination port and hostname.
interface TcpConnectOptions {
port: number
hostname?: string
}UdpBindOptions
Options for `udpBind`: local port (ephemeral if omitted) and bind hostname.
interface UdpBindOptions {
port?: number
hostname?: string
}