@signaldb/generic-fs
import createGenericFSAdapter from '@signaldb/generic-fs'
import type { Driver } from '@signaldb/generic-fs'The shared foundation behind @signaldb/fs and @signaldb/opfs. It turns a handful of low-level file operations into a full storage adapter, so a new file-like backend costs you a driver rather than an adapter.
Reach for it when you want to store a collection on something that can create directories, read and write files, and list them — a remote file API, an archive, a virtual filesystem — and you would rather not reimplement sharding and index maintenance.
createGenericFSAdapter (default)
createGenericFSAdapter<T, I>(driver: Driver<T, I>, folderName: string)driver: The low-level operations for your backend, see below.folderName: The folder this collection lives in.
Returns a StorageAdapter, ready to be handed to a data adapter:
import { Collection, DefaultDataAdapter } from '@signaldb/core'
import createGenericFSAdapter from '@signaldb/generic-fs'
const dataAdapter = new DefaultDataAdapter({
storage: name => createGenericFSAdapter(myDriver, `./data/${name}`),
})
const Posts = new Collection('posts', dataAdapter)Layout
Inside folderName the adapter keeps two directories:
items/— one file per document, sharded over two levels of subdirectory by the document's id, so a directory never grows without bound.index/— one file per indexed value.
A write therefore costs one file, not a rewrite of the whole collection.
Driver
interface Driver<T extends { id: I }, I> {
fileNameForId(id: I): Promise<string>
fileNameForIndexKey(key: string): Promise<string>
joinPath(...parts: string[]): Promise<string>
ensureDir(path: string): Promise<void>
fileExists(path: string): Promise<boolean>
readObject(path: string): Promise<T[] | null>
writeObject(path: string, value: T[]): Promise<void>
readIndexObject(path: string): Promise<Record<string, I[]>[] | null>
writeIndexObject(path: string, value: Record<string, I[]>[]): Promise<void>
listFilesRecursive(directoryPath: string): Promise<string[]>
removeEntry(path: string, options?: { recursive?: boolean }): Promise<void>
}- fileNameForId turns a document id into a path relative to
items/. This is where sharding happens:@signaldb/fsreturnsab/cd/abcdef…so that a large collection does not put every file in one directory. Whatever you return has to be a legal name for your backend — escape it. - fileNameForIndexKey does the same for an index key. Index keys are
serializeValue(value)and can contain anything a document field can, so this one always needs escaping. - joinPath joins path segments the way your backend spells paths.
- ensureDir creates a directory and its parents, and does nothing if it already exists.
- fileExists reports whether a path exists. Return
falserather than throwing when it does not. - readObject / writeObject read and write one document file.
readObjectreturnsnullwhen the file is missing — that is not an error. - readIndexObject / writeIndexObject do the same for an index file.
- listFilesRecursive returns every file under a directory, as paths relative to it.
- removeEntry deletes a file, or a directory with
{ recursive: true }.
Serialization is yours: readObject and writeObject decide what a file contains, which is how @signaldb/fs and @signaldb/opfs offer their serialize and deserialize options.
Example
@signaldb/fs is the shortest complete driver to read — it implements the interface above against Node's fs/promises in about eighty lines. @signaldb/opfs is the same interface against the browser's Origin Private File System.