@statewalker/webrun-content-store 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +88 -35
  2. package/package.json +11 -6
package/README.md CHANGED
@@ -1,20 +1,47 @@
1
1
  # @statewalker/webrun-content-store
2
2
 
3
- Domain-neutral, content-addressed large-object store with content-defined chunking and deduplication.
3
+ ## What it is
4
4
 
5
- ## Overview
5
+ A content-addressed store for large byte streams. It splits content into content-defined chunks,
6
+ stores each distinct chunk once, and keeps a manifest per object listing its chunks. Chunks and
7
+ manifests live in two `BlobStore`s from `@statewalker/webrun-storage`; object and chunk ids come
8
+ from a hash function you supply.
6
9
 
7
- `content-store` persists large byte streams as deduplicated, content-defined chunks over the `@statewalker/webrun-storage` `BlobStore` seam. It is **algorithm-agnostic**: the whole-object and per-chunk identities are computed by an **injected `hashContent`**, and every id is treated as an opaque string (it carries whatever prefix the hasher produces). The content-defined chunker lives in-package; content below a threshold is stored as a single direct blob.
10
+ ## Why it exists
8
11
 
9
- It is shared infrastructure for both axes: it backs Axis A's "ask which chunks exist → send only the missing → assemble" transfer flow (via `@statewalker/webrun-content-transfer`) and Axis B's LFS-pointer-to-object indirection (`@statewalker/vcs-transport-lfs` / `-xet`). It knows nothing of git, LFS, SHA-256, commits, or sync — those live in the skins above it.
12
+ Large files are expensive to store twice and to send whole. With content-defined chunking, two
13
+ versions of a file share most of their chunks, so storage grows by the changed region only, and a
14
+ peer that already has most chunks needs only the missing ones. This package provides that chunk
15
+ layer for any domain: it knows nothing of git, LFS or sync, and treats every id as an opaque
16
+ string. `@statewalker/webrun-content-transfer` moves its chunks between stores.
10
17
 
11
- ## Installation
18
+ ## How to use
12
19
 
13
20
  ```bash
14
- pnpm add @statewalker/webrun-content-store
21
+ pnpm add @statewalker/webrun-content-store @statewalker/webrun-storage
15
22
  ```
16
23
 
17
- ## Quick Start
24
+ | Import | Contents |
25
+ | --- | --- |
26
+ | `@statewalker/webrun-content-store` | `createContentStore` and the types `ContentStore`, `ContentStoreDeps`, `ContentStoreOptions`, `ObjectDescriptor`, `ChunkRef`, `ObjectId`, `ChunkId`, `CdcParams`, `ByteStream`. ESM, runs in browsers, Node and workers. |
27
+
28
+ `createContentStore(deps, opts?)`:
29
+
30
+ - `deps`: `{ chunks: BlobStore, manifests: BlobStore, hashContent }`.
31
+ - `opts`: `{ chunkThreshold?, cdc? }`. Defaults: `chunkThreshold` 4096 bytes,
32
+ `cdc` `{ min: 2048, avg: 8192, max: 32768 }`.
33
+
34
+ The returned `ContentStore`:
35
+
36
+ | Level | Methods |
37
+ | --- | --- |
38
+ | Object | `put(content)` returns `{ id, size, chunks }`; `read(id, { offset?, length? }?)`; `has(id)`; `getManifest(id)`; `remove(id)` |
39
+ | Chunk | `hasChunks(ids)` returns the ids that are **not** stored; `putChunk(id, bytes)`; `getChunk(id)` |
40
+ | Maintenance | `gc(liveRoots)` returns `{ removedObjects, removedChunks }` |
41
+
42
+ ## Examples
43
+
44
+ Store, read back and slice an object:
18
45
 
19
46
  ```typescript
20
47
  import { createHash } from "node:crypto";
@@ -26,43 +53,69 @@ async function sha256(bytes: AsyncIterable<Uint8Array>): Promise<string> {
26
53
  for await (const chunk of bytes) h.update(chunk);
27
54
  return `sha256:${h.digest("hex")}`;
28
55
  }
56
+ async function* streamOf(bytes: Uint8Array) {
57
+ yield bytes;
58
+ }
59
+
60
+ const store = createContentStore({
61
+ chunks: memBlobStore(),
62
+ manifests: memBlobStore(),
63
+ hashContent: sha256,
64
+ });
65
+
66
+ const payload = new Uint8Array(100_000).map((_, i) => (i * 7919) % 251);
67
+ const d = await store.put(streamOf(payload)); // { id, size: 100000, chunks: [...] }
68
+ const whole = store.read(d.id);
69
+ const slice = store.read(d.id, { offset: 30_000, length: 2_500 }); // crosses chunk borders
70
+ ```
29
71
 
30
- const store = createContentStore(
31
- { chunks: memBlobStore(), manifests: memBlobStore(), hashContent: sha256 },
32
- { chunkThreshold: 64, cdc: { min: 64, avg: 256, max: 1024 } },
33
- );
72
+ Find what a peer is missing, and clean up:
34
73
 
35
- async function* streamOf(bytes: Uint8Array) { yield bytes; }
74
+ ```typescript
75
+ const missing = await store.hasChunks(d.chunks.map((c) => c.id)); // [] here: all present
76
+ await store.remove(d.id); // removes the manifest only
77
+ await store.gc([]); // no live roots: removes every remaining object and chunk
78
+ ```
36
79
 
37
- // Object level: chunk + hash in one bounded-memory pass.
38
- const d = await store.put(streamOf(payload)); // ObjectDescriptor { id, size, chunks }
39
- const whole = store.read(d.id); // reassembled stream
40
- const slice = store.read(d.id, { offset: 3000, length: 2500 }); // arbitrary range across chunks
80
+ ## Internals
41
81
 
42
- // Chunk level: negotiate and move only what a peer is missing.
43
- const missing = await store.hasChunks(d.chunks.map((c) => c.id)); // ids NOT present
44
- await store.gc([d.id]); // sweep everything not reachable from the live roots
82
+ ### How content is cut into chunks
83
+
84
+ ```
85
+ bytes ──> Gear rolling hash ──> cut where low bits are zero ──> chunks (min..max bytes)
86
+ (restarts at every cut) (stricter mask below avg, looser above)
45
87
  ```
46
88
 
47
- ## API
89
+ The chunker is a minimal FastCDC: a Gear rolling fingerprint that restarts at each boundary, with
90
+ normalized masks that pull chunk sizes toward `avg`. Because a cut depends only on the bytes since
91
+ the previous cut, boundaries re-synchronise shortly after an insert or delete, and the chunks
92
+ around the edit keep their ids. Content strictly smaller than `chunkThreshold` is stored as one
93
+ chunk; chunking small files only adds manifest overhead.
94
+
95
+ ### Memory stays bounded
96
+
97
+ `put` reads the input one chunk at a time. The whole-object id is computed by re-streaming the
98
+ stored chunks in order through `hashContent`, so an object is never held in memory in one piece.
99
+ `read` with a range skips chunks outside the range.
100
+
101
+ ### Ids are opaque
102
+
103
+ Object and chunk ids are whatever `hashContent` returns, including any prefix such as `sha256:`.
104
+ The store commits to no algorithm. Use the same `hashContent` for every store that exchanges
105
+ chunks; ids from different hashes never match.
48
106
 
49
- **Object level**
50
- - `put(content): Promise<ObjectDescriptor>` — chunk + hash in a bounded-memory pass, store chunks + manifest.
51
- - `read(id, range?)` — stream the object's bytes, optionally sliced by `{ offset?, length? }`; empty stream if absent.
52
- - `has(id)`, `getManifest(id)`, `remove(id)`.
107
+ ### Liveness is external
53
108
 
54
- **Chunk level** (transfer / dedup)
55
- - `hasChunks(ids): Promise<ChunkId[]>` — return exactly the ids **not** present.
56
- - `putChunk(id, bytes)`, `getChunk(id)`.
109
+ There are no reference counts. `remove(id)` deletes only the manifest: chunks stay, because
110
+ another object may share them. `gc(liveRoots)` marks every chunk reachable from the manifests you
111
+ list and deletes all other manifests and chunks. Anything not in `liveRoots` is removed, so pass
112
+ the complete set of objects you still need.
57
113
 
58
- **Maintenance**
59
- - `gc(liveRoots): Promise<{ removedObjects, removedChunks }>` — mark-sweep from caller-supplied roots.
114
+ ### Dependencies
60
115
 
61
- **Factory:** `createContentStore(deps, opts?)`, where `deps` is `{ chunks: BlobStore, manifests: BlobStore, hashContent }` and `opts` is `{ chunkThreshold?, cdc? }`. Types: `ContentStore`, `ObjectDescriptor`, `ChunkRef`, `ObjectId`, `ChunkId`, `CdcParams`.
116
+ `@statewalker/webrun-storage`, for the `BlobStore` type. `@statewalker/webrun-files` is declared in
117
+ `package.json` but not imported by the sources. The chunker and manifest format are in-package. No runtime dependencies.
62
118
 
63
- ## Notes
119
+ ## License
64
120
 
65
- - **Injected hash, opaque ids.** The contract sketch spoke of intrinsic BLAKE3; the shipped code takes an injected `hashContent` and treats every id as opaque — the store commits to no algorithm, and object/chunk ids simply carry the hasher's own prefix.
66
- - **CDC + direct-blob threshold.** Content-defined chunking (so dedup survives inserts/shifts) is the default; content strictly below `chunkThreshold` is stored as a single blob with no chunking overhead. `hasChunks`/`getChunk`/`putChunk` let `content-transfer` move only missing chunks.
67
- - **Immutable + external liveness.** Chunks and objects are immutable and content-addressed; there are no refcounts — `gc(liveRoots)` reaches objects→chunks from caller roots and sweeps the rest.
68
- - Built red/green TDD.
121
+ MIT
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-content-store",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Domain-neutral, content-addressed large-object store with content-defined chunking over the storage BlobStore",
5
+ "bugs": {
6
+ "url": "https://github.com/statewalker/webrun-sync/issues"
7
+ },
8
+ "homepage": "https://github.com/statewalker/webrun-sync/tree/main/packages/webrun-content-store#readme",
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
- "url": "https://github.com/statewalker/webrun-vcs.git",
12
+ "url": "git+https://github.com/statewalker/webrun-sync.git",
9
13
  "directory": "packages/webrun-content-store"
10
14
  },
11
15
  "private": false,
@@ -27,11 +31,11 @@
27
31
  "src"
28
32
  ],
29
33
  "dependencies": {
30
- "@statewalker/webrun-files": "^0.10.0",
31
- "@statewalker/webrun-storage": "^0.2.1"
34
+ "@statewalker/webrun-files": "^0.12.0",
35
+ "@statewalker/webrun-storage": "^0.3.0"
32
36
  },
33
37
  "devDependencies": {
34
- "@statewalker/webrun-files-mem": "^0.10.0",
38
+ "@statewalker/webrun-files-mem": "^0.12.0",
35
39
  "rolldown": "^1.2.12",
36
40
  "vitest": "^5.0.3",
37
41
  "typescript": "^7.0.2",
@@ -46,6 +50,7 @@
46
50
  "format": "biome format --write .",
47
51
  "format:check": "biome format .",
48
52
  "format:fix": "biome check --write --unsafe .",
49
- "typecheck": "tsc --noEmit"
53
+ "typecheck": "tsc --noEmit",
54
+ "typecheck:tests": "tsc -p tsconfig.tests.json"
50
55
  }
51
56
  }