Initial Commit
This commit is contained in:
126
packages/persistence/storage/storage-file/src/bun-fs/bun-fs.md
Normal file
126
packages/persistence/storage/storage-file/src/bun-fs/bun-fs.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# `bun-fs` — Bun-native `NodeFsLike` adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`bun-fs` is a sibling of [`node-fs`](../node-fs/node-fs.md) 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
|
||||
|
||||
```ts
|
||||
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`:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user