Files
suaveplan/openspec/archive/2026-05-16-add-storage-file-bun-fs-adapter/proposal.md
2026-05-27 23:29:53 +01:00

76 lines
3.4 KiB
Markdown

# Add Bun-native filesystem adapter to `@suaveplan/storage-file`
- **Capability**: `storage-file`
- **Status**: PROPOSED
- **Design**: ./design.md
- **Spec delta**: ./specs/storage-file/spec.md
## Why
`@suaveplan/storage-file` currently ships a single `NodeFsLike` implementation —
`defaultNodeFs()` in `src/node-fs/node-fs.ts` — bound to `node:fs/promises` plus
the two `node:fs` stream constructors. Genesis is a Bun-first monorepo. Bun
provides a faster, non-emulated filesystem path for the common read-and-write
hot loop via `Bun.file()` / `Bun.write()` that skips the `node:fs`
emulation layer entirely. Today every read/write inside `NodeFsStorage` takes
the slower compatibility path even when running under Bun.
This change adds a sibling `defaultBunFs()` factory that satisfies the same
`NodeFsLike` structural contract but routes `readFile`/`writeFile` through
Bun's native APIs. Operations that have no Bun-specific equivalent
(`mkdir`, `readdir`, `rename`, `unlink`, `stat`, `access`, `open`,
`createReadStream`, `createWriteStream`) continue to use `node:fs/promises` —
under Bun those calls already dispatch to Bun's native Zig implementation.
`resolveConfig` gains a runtime-probed default: when the caller does not
inject an `fs` and `globalThis.Bun` is present, `defaultBunFs()` is used;
otherwise `defaultNodeFs()` is used. Both factories remain explicitly
exported so callers can wire either one directly (e.g. forcing the Node
path in tests, or in mixed-runtime SSR pipelines).
## What changes
- New module `src/bun-fs/bun-fs.ts` exporting `defaultBunFs(): NodeFsLike`.
- `readFile` uses `Bun.file(path).bytes()`.
- `writeFile` uses `Bun.write(path, data)`.
- All other methods delegate to `node:fs/promises` (the same calls
`defaultNodeFs()` makes), so the surface stays identical.
- New module `src/bun-fs/bun-fs.md` (sibling doc, §4 conformant).
- New module `src/bun-fs/bun-fs.test.ts` (`@suaveplan/testing/runner`).
- New barrel `src/bun-fs/index.ts` (explicit named re-export per §6a).
- `resolveConfig` in `src/node-fs-storage/node-fs-storage.helpers.ts` gains a
Bun-vs-Node default selector. Behaviour is unchanged when callers pass
`fs` explicitly.
- `src/index.ts` re-exports `defaultBunFs` next to `defaultNodeFs`.
- `package.json` `exports` map gains `./bun-fs/bun-fs`.
- `README.md` Modules section gets a new row.
## Migration
No consumer behaviour changes if `fs` is passed explicitly. Consumers that
relied on the implicit default got a `node:fs/promises`-backed implementation
before this change and continue to get one when running under Node. Under Bun
they transparently get the faster path. The `NodeFsLike` contract, all
`STORAGE_FILE.*` codes, the `recordOp` instrumentation, and the spec-conformance
suite are unchanged.
To force the Node path under Bun (e.g. for parity testing):
```ts
import { defaultNodeFs, NodeFsStorage } from "@suaveplan/storage-file";
const storage = new NodeFsStorage({ rootDir, fs: defaultNodeFs() });
```
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/storage-file` to enumerate. As of
authoring, `@suaveplan/storage-file` has no in-repo dependents (it is consumed
by the upstream `@suaveplan/storage` factory via the optional registry path).
- No in-repo dependents — verified by `bunx turbo ls --filter=...@suaveplan/storage-file`.
Storage-cdn-bunnycdn and storage-cdn-rackspace consume `@suaveplan/storage`,
not `storage-file` directly; their behaviour is unaffected.