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.
Methods
Section titled “Methods”writeFile(input)
Section titled “writeFile(input)”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, fileId)
Section titled “readFile(spaceId, fileId)”readFile(spaceId: string, fileId: string): Promise<{ data: Uint8Array; stat: FileStat }>Fetch the manifest from the space, then reassemble the file from shards.
readByManifest(manifest)
Section titled “readByManifest(manifest)”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?)
Section titled “listFiles(opts?)”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, fileId)
Section titled “deleteFile(spaceId, fileId)”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, fileId)
Section titled “getManifest(spaceId, fileId)”getManifest(spaceId: string, fileId: string): Promise<FileManifest>Fetch a manifest without downloading the file — e.g. to share it.
How reads are fetched
Section titled “How reads are fetched”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:
| Requests | Chromium | Brave | |
|---|---|---|---|
| Before | 2,086 | 18.4s | 37.5s |
| After | 22 | 8.8s | 14.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.
FileStoragekeeps 8 shard reads in flight by default, andclient.vfsfetches 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;}