Files
suaveplan/packages/persistence/storage/storage-file/src/bun-fs/bun-fs.md
2026-05-27 23:47:53 +01:00

4.7 KiB

bun-fs — Bun-native NodeFsLike adapter

(no submodules)

Purpose

bun-fs is a sibling of node-fs that satisfies the same structural NodeFsLike contract consumed by NodeFsStorage. Where defaultNodeFs() wires every method to node:fs/promises and the two node:fs stream constructors, defaultBunFs() overrides only the two methods that have a measurably faster Bun-native path — readFile (routed through Bun.file(path).bytes()) and writeFile (routed through Bun.write(path, data)). Every other method delegates to the Node base because under Bun those calls already dispatch to Bun's native (Zig) implementation transparently via the Node-compat layer, leaving no perf on the table.

This module exists so Suaveplan — which targets Bun as its first runtime — does not pay the Node-compat reshaping cost on the hot read/write loop. It is additive: no behaviour changes for callers that inject a custom fs, and the Node adapter remains the explicit choice when running outside Bun or when parity testing under Bun.

Features

  • defaultBunFs(): NodeFsLike — Bun-native adapter. Throws a plain Error if called outside a Bun runtime.
  • pickDefaultFs(): NodeFsLike — runtime-probed selector. Returns defaultBunFs() when globalThis.Bun is defined, otherwise defaultNodeFs(). The probe runs on every call (never cached) so tests can switch adapters deterministically.
  • Returns a fresh object per call so callers can selectively override any single method without mutating shared state.
  • Zero runtime branching inside individual methods — composition over conditionals.

Usage: Basic Example

import { NodeFsStorage, defaultBunFs } from "@suaveplan/storage-file";

const storage = new NodeFsStorage({
    rootDir: "/var/data",
    fs: defaultBunFs(),
});

await storage.set("hello", new TextEncoder().encode("world"));
const bytes = await storage.get("hello");
console.log(new TextDecoder().decode(bytes!)); // "world"

Usage: Advanced Example

Combine runtime detection with a custom override — useful when you want the Bun fast path everywhere except a tracing wrapper around writeFile:

import {
    NodeFsStorage,
    pickDefaultFs,
    type NodeFsLike,
} from "@suaveplan/storage-file";

function tracedFs(): NodeFsLike {
    const base = pickDefaultFs();
    return {
        ...base,
        async writeFile(path, data) {
            const start = performance.now();
            try {
                await base.writeFile(path, data);
            } finally {
                console.info("writeFile", path, performance.now() - start);
            }
        },
    };
}

const storage = new NodeFsStorage({
    rootDir: "/var/data",
    fs: tracedFs(),
});

When you construct NodeFsStorage without passing fs, resolveConfig calls pickDefaultFs() for you — Bun-first, with Node fallback.

API Reference

defaultBunFs(): NodeFsLike

  • Returns a fresh NodeFsLike whose readFile is implemented as Bun.file(path).bytes() and whose writeFile is implemented as Bun.write(path, data). Every other method (mkdir, readdir, rename, unlink, stat, access, open, createReadStream, createWriteStream) delegates to the defaultNodeFs() base.
  • Throws a plain Error with the message defaultBunFs() requires a Bun runtime (globalThis.Bun is undefined) if the probe finds no Bun global. The intended entry point for adapter selection is pickDefaultFs, which performs the probe first.

pickDefaultFs(): NodeFsLike

  • Returns defaultBunFs() when globalThis.Bun is an object, otherwise defaultNodeFs(). Consumed by resolveConfig in node-fs-storage.helpers.ts to wire the default fs for NodeFsStorage.

Implementation Notes

  • Streams are NOT routed through Bun.file(path).stream(). BunFile.stream() returns a Web ReadableStream, but NodeFsLike.createReadStream is typed as a Node Readable. Translating between the two introduces a copy that erases the speedup, so the Node node:fs constructor is used unchanged — it already dispatches to Bun's native impl under Bun.
  • The detection probe uses typeof globalThis.Bun === "object" rather than "undefined" !== typeof Bun. The former works without TypeScript ambient globals and is robust to module-scope mocking via (globalThis as { Bun?: unknown }).Bun = undefined.
  • Bun.write accepts both Uint8Array and string, matching NodeFsLike.writeFile's union type. No buffer copying is performed by the adapter.
  • The adapter has zero @suaveplan/* runtime dependencies. It is internally consumed by node-fs-storage.helpers.resolveConfig, which is the only module that should call pickDefaultFs directly.