Skip to content
UniKVS
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

@unikvs/fs.node

Learn how to use the storage plugin that saves byte sequences to the local file system in Node.js.

Overview

@unikvs/fs.node is a storage plugin that saves data to the local file system in Node.js. Keys are stored as file names under the specified root directory.Node.js only

It handles byte sequences only. write accepts a Uint8Array<ArrayBuffer> and read returns a Uint8Array<ArrayBuffer>. The implementation dynamically imports node:fs, node:path, node:crypto, and node:stream.

Install it as follows.

npm install @unikvs/fs.node
pnpm add @unikvs/fs.node
yarn add @unikvs/fs.node
bun add @unikvs/fs.node
nub add @unikvs/fs.node
aube add @unikvs/fs.node

It depends on @unikvs/core and @unikvs/utils.

Usage

The central class is NodeFs.

import { NodeFs } from "@unikvs/fs.node";
public constructor(root: string = ".unikvs", options: NodeFsOptions = {})

root is the root directory for data. Relative paths are resolved against the current working directory when open() runs. The default is ".unikvs".

options.allowRepair controls whether repair writes are allowed. The default is false. When false, write and getWritable with a repair marker (vars["unikvs:repair"] is true) fail with RepairNotAllowedError.

The main members are as follows.

  • name is "NodeFs".
  • allowRepair indicates whether repair writes are allowed.
  • isOpen indicates whether the storage is open.
  • open() recursively creates the root directory and makes it ready for reads and writes.
  • write, read, exists, delete, and clear handle data.
import { NodeFs } from "@unikvs/fs.node";

const storage = new NodeFs("./.tmp/unikvs-fs");
await storage.open();

await storage.write({
  key: "hello.bin",
  data: new TextEncoder().encode("hello"),
});

const data = await storage.read({ key: "hello.bin" });
console.log(new TextDecoder().decode(data));

File Layout

Keys map to file paths as follows. Keys sit directly under the root with no subdirectories.

  • .unikvs/
    • hello.bin
    • greeting.bin
  • The final path is path.join(this.root, key).
  • The temporary write path is `${dest}.${crypto.randomUUID()}.tmp`. It is created in the same directory so rename stays atomic.
  • open() absolutizes with path.resolve(this.root) and creates the root with mkdir(root, { recursive: true }).
  • clear() removes the root with rm(this.root, { recursive: true, force: true }) and recreates it.

File names are validated with assertValidFilename from @unikvs/utils. Each of write, read, exists, delete, getWritable, and getReadable validates first. Invalid names throw InvalidFilenameError. Empty strings, ., .., names over 255 bytes, non-NFC strings, and reserved/Windows-reserved names cannot be used.

Streams

Both readable and writable streams are supported.

Obtain a writable stream with the following method.

public async getWritable(
  args: Pick<IStorage.GetWritableArgs, "key"> & { signal?: AbortSignal },
): Promise<WritableStream<Uint8Array<ArrayBuffer>>>

It uses swap-on-close: data is written to a temporary file and renamed to the final path only on successful close. If the AbortSignal aborts, the stream is discarded and the temp file is removed. It supports backpressure and never destroys existing data on failure or abort.

Obtain a readable stream with the following method.

public getReadable(
  args: Pick<IStorage.GetReadableArgs, "key" | "signal">,
): ReadableStream<Uint8Array<ArrayBuffer>>

It converts fs.createReadStream(file, { signal }) to a web stream with stream.Readable.toWeb(readStream). Aborting signal destroys the read stream with the abort reason, so in-flight reads fail as well.

Notes

  • Don’t call write or read before open(). The internal connection is null until then. Check with isOpen.
  • open() re-initializes even when already open. root is kept as an absolute path.
  • write writes to a temp file then renames. On failure or abort, the temp file is removed, preserving existing data.
  • Concurrent writes to the same key don’t collide thanks to unique suffixes. The last rename wins.
  • exists returns false on access(file) errors.
  • delete fails for missing files.
  • Permissions follow OS and Node.js defaults.
  • close() is not implemented. Since close in IStorage is optional, manage cleanup yourself when needed.

Examples

A minimal example storing under .tmp/.

import { NodeFs } from "@unikvs/fs.node";

const storage = new NodeFs("./.tmp/unikvs-fs-node-example");
await storage.open();

const key = "greeting.bin";
await storage.write({
  key,
  data: new TextEncoder().encode("hello, NodeFs"),
});

if (await storage.exists({ key })) {
  const data = await storage.read({ key });
  console.log(new TextDecoder().decode(data));
}

await storage.delete({ key });
await storage.clear();

Was this page helpful?