Skip to content

client.storage

See the Files guide for the model. For small structured values, use client.kv.

Files are chunked, AES-256-GCM-encrypted, and Reed–Solomon-erasure-coded into a global, open, content-addressed shard store; the manifest (per-chunk keys + shard hashes) is the capability. The manifest is stored in the Space’s gated store in the clear, so Muhkoo can read it (and therefore the files) — only the shard store alone is opaque. Files are written to a SharedSpace (metered against it) and mirrored into the caller’s own space for discovery.

writeFile(input: {
spaceId: string;
data: Uint8Array | Blob | File;
metadata: { name: string; type: string; path?: string; lastModified?: number };
}): Promise<{ stat: FileStat; manifest: FileManifest }>

Chunk + encrypt + erasure-code the data into the global shard store, write the manifest into spaceId’s gated store, and mirror it into the caller’s personal index (a best-effort client.kv write under a reserved collection; if the mirror fails the file is still written). spaceId is required — omitting it throws, since files live in a Space (get one from client.space). Returns the stat and the manifest (share the manifest to let others read the file).

readFile(spaceId: string, fileId: string): Promise<{ data: Uint8Array; stat: FileStat }>

Fetch the manifest from the space, then reassemble the file from shards.

readByManifest(manifest: FileManifest): Promise<{ data: Uint8Array; stat: FileStat }>

The capability read: reassemble a file straight from a manifest, reading the global shards. No space membership required — this is how a shared file is opened by a recipient.

listFiles(opts?: { spaceId?: string }): Promise<FileStat[]>

With no argument, lists the caller’s own shared files (from the personal discovery mirror). With { spaceId }, lists that space’s files.

deleteFile(spaceId: string, fileId: string): Promise<boolean>

Removes the manifest from the space and the caller’s mirror entry. Resolves to whether the file existed. Shards are content-addressed and may be referenced by other manifests, so they are not deleted here (server-side GC is separate).

getManifest(spaceId: string, fileId: string): Promise<FileManifest>

Fetch a manifest without downloading the file — e.g. to share it.

A shard is not a file. A file is split into chunks and every chunk is erasure-coded into shards, so a project of small source modules is a very large number of very small shards. Two things stop that becoming one request per shard.

Only the data shards are fetched. Reed–Solomon needs dataShards of the dataShards + parityShards a chunk was encoded into, and the data shards come first in the manifest. Parity is fetched only when a data shard did not arrive — one extra round trip in the rare damaged case, instead of two wasted ones on every healthy read. With the defaults (4 data, 2 parity) a chunk costs four fetches rather than six. A file smaller than a single shard used to pay all six regardless, which is why small source modules were the worst case rather than the best.

Concurrent reads coalesce into one request. Shard reads issued within a few milliseconds of each other are gathered and sent as a single POST /api/shards/batch, up to 256 hashes at a time. Nothing in the calling code changes — readFile, readByManifest and client.vfs are untouched, and the transport underneath still exposes a one-shard-at-a-time getShard. A hash the store does not hold comes back reported as missing rather than failing the whole batch, so the caller can still rebuild that chunk from parity.

Measured opening the same 519-file project in the Muhkoo IDE:

RequestsChromiumBrave
Before2,08618.4s37.5s
After228.8s14.9s

Brave gains the most because it filters every request — the cost was the number of round trips, not the bytes.

Two consequences worth designing around:

  • Read files in parallel, not in sequence. Awaiting one read before starting the next hands the batcher a single hash at a time, which is the one case batching cannot help. Issue the reads together (Promise.all, or a bounded map) and they coalesce.
  • Concurrency and batching have to be tuned together. Batching with too few reads in flight is worse than not batching at all: it trades parallelism for a round trip. FileStorage keeps 8 shard reads in flight by default, and client.vfs fetches directory records concurrently while it walks for the same reason — the walk is what discovers the files whose shards then batch together, so a narrow walk throttles everything downstream. (In 0.14.0-alpha.8 a walk lists up to 8 directories at a time per level; from 0.14.0-alpha.9, one walk shares a single pool of 64 fetches — see Walking a tree.)

Local sources come first and never reach a batch: a shard in the offline cache is served from there. Files read through a Space (space.getFile) also try peers before origin when peer transfer is enabled; reads through client.storage and client.vfs do not use peers. Batching applies to whatever is left.

Large files read two chunks ahead. A multi-chunk read (readFile, readByManifest, and the chunk stream a Space exposes as getFileStream) keeps up to two further chunks in flight while the current one is decoded, rather than fetching them one at a time — about 40% faster on a large file. A read that stops early starts no more fetches, and at most three chunks are held in memory for a stream.

If you construct the transport yourself, ShardClient takes batchShards?: boolean (default true) to turn coalescing off. You rarely want to — against a deployment without the batch route the client detects that on its first attempt and falls back to one-at-a-time for the rest of the session, so compatibility is already handled.

interface FileStat {
id: string;
name: string;
size: number; // total plaintext bytes
type: string; // MIME type the app supplied
path?: string;
lastModified: number;
createdAt: number;
chunkCount: number;
shardCount: number;
}
interface FileManifest {
id: string;
name: string;
size: number;
type: string;
path?: string;
lastModified: number;
createdAt: number;
chunks: ChunkManifest[]; // per-chunk keys, IVs, RS params, shard hashes
version: 1;
}