prolly-map 0.2.0

Content-addressed versioned map storage primitives.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# Async store support

This document expands the async-store roadmap item into a concrete design track.
The goal is to support remote, browser, object-store, and background-agent
workloads without forcing async dependencies or runtime assumptions onto the
current embedded `Store` API.

## Why Async Store Support Matters

The current `Store` trait is synchronous, which is a good fit for in-process
and embedded backends such as memory, SQLite, and RocksDB. AI-native and
local-first applications also need storage patterns where blocking APIs become
awkward or inefficient:

- S3, R2, GCS, and other object stores.
- Remote peer sync over HTTP, WebSocket, or custom transports.
- Browser and WASM storage APIs.
- Network caches and edge storage.
- Background agents that should overlap many node reads.
- Long-running diff, sync, and indexing tasks that need backpressure.

Prolly trees are especially well suited to these environments because immutable
content-addressed nodes can be fetched, cached, exchanged, and verified
independently.

## Design Principles

- Keep the sync `Store` trait stable and useful.
- Put async support behind an optional feature such as `async-store`.
- Avoid a hard dependency on Tokio in the core async API.
- Preserve ordered batch-read semantics; many tree algorithms depend on result
  order matching request order.
- Preserve correctness if stores ignore hints, batching, or prefetching.
- Make concurrency explicit and bounded.
- Support object stores where immutable node writes and mutable root updates
  have different consistency models.
- Leave room for browser/WASM stores that may not be `Send`.

## Sync vs Async Tradeoffs

Async storage does not make CPU-bound tree work faster by itself. It helps when
the workload spends meaningful time waiting on storage, network, browser APIs,
or remote object stores. In those cases, async lets the tree overlap multiple
node reads and keep the application runtime responsive.

Sync `Store` benefits:

- Smallest dependency surface and easiest API for embedded use.
- Excellent fit for in-process memory, SQLite, and RocksDB.
- Simpler error paths and easier debugging.
- Lower overhead for CPU-bound or already-local operations.
- Works well in CLI tools, benchmarks, and synchronous host applications.

Sync `Store` costs:

- Blocking calls can stall async runtime worker threads if used directly.
- Harder to overlap many remote node reads without application-level threading.
- Awkward for browser/WASM, HTTP, WebSocket, and object-store APIs.

Async `Store` benefits:

- Natural fit for S3/R2/GCS, remote sync, network caches, and browser storage.
- Can overlap independent node fetches with bounded concurrency.
- Keeps async applications responsive during storage waits.
- Enables background agents and sync tasks to share a runtime cleanly.

Async `Store` costs:

- More API surface and more complex test matrix.
- Slight overhead for futures and scheduling.
- Runtime-specific adapters must be handled carefully.
- Blocking embedded stores still need an adapter such as `TokioBlockingStore`
  when used from async applications.

Default guidance: keep Tokio optional. The core async trait should stay
runtime-neutral, while the `tokio` feature provides the convenient
`TokioBlockingStore<S>` adapter for applications that already use Tokio.

## Proposed API Shape

The async API should be parallel to `Store`, not a replacement for it.

```rust
pub trait AsyncStore {
    type Error: std::error::Error + 'static;

    async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;

    async fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error>;

    async fn delete(&self, key: &[u8]) -> Result<(), Self::Error>;

    async fn batch(&self, ops: &[BatchOp<'_>]) -> Result<(), Self::Error>;

    async fn batch_get_ordered(
        &self,
        keys: &[&[u8]],
    ) -> Result<Vec<Option<Vec<u8>>>, Self::Error>;

    async fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error>;

    fn prefers_batch_reads(&self) -> bool {
        false
    }

    fn read_parallelism(&self) -> usize {
        1
    }

    async fn batch_get_ordered_unique(
        &self,
        keys: &[&[u8]],
    ) -> Result<Vec<Option<Vec<u8>>>, Self::Error>;

    async fn get_hint(
        &self,
        namespace: &[u8],
        key: &[u8],
    ) -> Result<Option<Vec<u8>>, Self::Error>;

    async fn put_hint(
        &self,
        namespace: &[u8],
        key: &[u8],
        value: &[u8],
    ) -> Result<(), Self::Error>;

    async fn batch_put_with_hint(
        &self,
        entries: &[(&[u8], &[u8])],
        namespace: &[u8],
        key: &[u8],
        value: &[u8],
    ) -> Result<(), Self::Error>;
}
```

The exact Rust representation needs a small prototype before stabilization.
Options:

- Use `async fn` in the public trait for the cleanest source API.
- Use associated future types if the crate needs more control over `Send`
  bounds and allocations.
- Add a boxed dynamic adapter later if applications need
  `Arc<dyn DynAsyncStore<...>>`.

The first implementation uses `async fn` in the public trait and keeps the base
trait free of `Send`/`Sync` bounds so browser and WASM stores can implement it.
Native managers or backends can add stronger bounds when they need cross-thread
execution.

## Async Prolly Manager

Add a separate manager instead of making `Prolly<S>` dual-mode:

```rust
pub struct AsyncProlly<S> {
    store: S,
    config: Config,
    // async-aware cache and hint state
}
```

Current methods:

- `create`
- `get`
- `get_many`
- `range`
- `range_after`
- `range_from_cursor`
- `range_page`
- `diff`
- `range_diff`
- `stream_diff`
- `structural_diff_page`
- `merge`
- `crdt_merge`
- `put`
- `delete`
- route-planned coalesced `batch`
- `collect_stats`
- `stats_diff`
- `cache_len`
- `cache_bytes_len`
- `cache_pinned_len`
- `cache_pinned_bytes_len`
- `clear_cache`
- `pin_tree_root`
- `pin_tree_path`
- `unpin_all_cache_nodes`

The implementation should share as much non-I/O logic as possible with the
sync engine. Any pure operations such as node search, mutation preprocessing,
conflict resolution, boundary detection, and serialization should remain
runtime-agnostic.

## Adapters

Adapters make adoption less abrupt.

### Sync Store as Async Store

Wrap an existing `Store` and expose it as `AsyncStore`.

Use cases:

- tests;
- migration;
- apps that already run inside async tasks but use SQLite or memory stores;
- validating that async algorithms match sync algorithms.

This adapter should not spawn blocking work by default. A runtime-specific
adapter can be added later for `spawn_blocking` behavior.

### Tokio Blocking Store Adapter

The optional `tokio` feature provides `TokioBlockingStore<S>`, which adapts a
blocking `Store` to `AsyncStore` by running each sync store call through
`tokio::task::spawn_blocking`.

Use this when an async application wants to use an embedded blocking backend
such as SQLite, RocksDB, or an in-process test store without blocking runtime
worker threads.

The default crate remains runtime-free; Tokio is opt-in.

### Async Store as Sync Store

This is less desirable and should be optional, if it exists at all. Blocking on
async storage can deadlock or accidentally tie the crate to a runtime. Prefer
documenting this as an application-level adapter rather than a core feature.

### Arc Support

Mirror the sync implementation:

```rust
impl<T: AsyncStore> AsyncStore for Arc<T> { ... }
```

## Batch Reads and Concurrency

The current sync `Store` has `batch_get_ordered`, `batch_get_ordered_unique`,
and `prefers_batch_reads`. Async support should preserve those semantics and
add explicit concurrency limits.

Important behaviors:

- deduplicate repeated CIDs before fetch;
- preserve caller order after fetch;
- cap in-flight requests;
- allow stores to override with native multi-get;
- expose read parallelism as a store preference;
- avoid unbounded fanout during broad diff or range scans.

Potential API:

```rust
pub struct AsyncReadConfig {
    pub max_in_flight_reads: usize,
    pub prefetch_child_frontiers: bool,
}
```

Default behavior should be conservative. Object stores and remote sync stores
can opt into higher concurrency.

## Object Store Backend Pattern

Object stores fit prolly nodes well because nodes are immutable and addressed by
CID.

Recommended layout:

```text
nodes/sha256/ab/cd/<cid-bytes>
hints/<namespace>/<key>
manifests/<name>
blobs/sha256/ab/cd/<blob-cid>
```

Design notes:

- Node writes are idempotent.
- Reads can verify CID by hashing bytes after fetch.
- Node writes can be massively parallel.
- Mutable root updates belong in a manifest layer, not in node storage.
- Garbage collection should mark from manifest roots before deleting objects.
- Hints are optional and can be stale.

Open question: whether the first object-store backend should depend on the
`object_store` crate or provide a smaller trait for application-owned clients.

## Async Blob Store Pattern

Large-value blobs use a separate async abstraction from nodes. `AsyncBlobStore`
mirrors `BlobStore` for content-addressed payload bytes, while keeping the
runtime-neutral `async-store` feature:

- native object stores can implement async point reads/writes directly;
- `get_blobs_ordered` deduplicates repeated `BlobRef`s, preserves caller order,
  and can overlap point reads using `read_parallelism`;
- `SyncBlobStoreAsAsync` adapts embedded blob stores without spawning;
- `TokioBlockingBlobStore` is available behind the `tokio` feature for
  blocking blob backends inside Tokio applications;
- `AsyncProlly` resolves and writes large values through async blob stores and
  can run blob mark/plan/sweep against async blob candidates.

Blob GC remains candidate-driven for the same reason node GC does: object-store
listing, application blob indexes, and shared blob namespaces have different
ownership rules.

## Browser and WASM Storage

Browser storage needs slightly different constraints:

- APIs may be async but not `Send`.
- IndexedDB transactions have their own lifetime rules.
- storage quotas and eviction are real concerns.
- large values should probably be offloaded or chunked.

The async design should avoid requiring Tokio or `Send` futures at the lowest
trait layer. If native multi-threaded async needs `Send`, expose that as an
additional bound on the manager or helper methods rather than on every store.

## Remote Sync Use Cases

Async store support should pair naturally with sync primitives:

1. Compare root CIDs.
2. Traverse changed subtrees.
3. Request missing node CIDs concurrently.
4. Verify fetched nodes by CID.
5. Write missing nodes locally.
6. Merge roots through standard or CRDT policies.
7. Update a manifest root with compare-and-swap where supported.

This gives AI applications an efficient way to sync agent memory, document
indexes, conversation history, and derived metadata.

## Implementation Phases

### Phase 1: Trait and Test Harness

- [x] Add an `async-store` feature.
- [x] Add `AsyncStore` trait.
- [x] Add async equivalents for ordered batch-read planning.
- [x] Add `Arc<T>` support.
- [x] Add a sync-store-to-async adapter.
- [x] Add optional Tokio `spawn_blocking` adapter for blocking stores.
- [x] Add initial conformance tests that compare async behavior with `Store`.
- [x] Add `AsyncBlobStore` for native async large-value/object-store backends.
- [x] Add `SyncBlobStoreAsAsync` and optional `TokioBlockingBlobStore` adapters.
- [x] Add async large-value and blob-GC helpers on `AsyncProlly`.

### Phase 2: Async Reads

- [x] Add `AsyncProlly<S>`.
- [x] Implement `create`, `get`, and `get_many`.
- [x] Implement range scan with `next().await` and `into_stream()`.
- [x] Implement resumable async range cursors and bounded range pages.
- [x] Implement `collect_stats` and `stats_diff`.
- [x] Add cache metrics for async reads.

### Phase 3: Async Writes

- [x] Implement `put` and `delete`.
- [x] Implement initial sequential async batch mutation application.
- [x] Add async batch write collector.
- [x] Replace sequential async batch with a single-flush full-rebuild batch.
- [x] Port route-planned coalesced async batch mutation for large trees.
- [x] Preserve one-call batch write semantics where the backend supports it.
- [x] Add append-heavy rightmost-path support with async hints.

### Phase 4: Async Diff and Merge

- [x] Implement async eager structural diff.
- [x] Implement async streaming diff.
- [x] Implement async range diff.
- [x] Implement async three-way merge with the existing resolver model.
- [x] Implement async CRDT merge.
- [x] Implement async node reachability and missing-node copy helpers.
- [x] Add bounded child-frontier prefetch for broad async traversals.

### Phase 5: Backends

- [x] Add generic store-to-store missing-node sync primitives.
- [x] Add local object-layout node backend pattern (`FileNodeStore`).
- [ ] Add object-store backend prototype.
- [ ] Add HTTP or remote peer store prototype.
- [ ] Add browser/WASM storage prototype or example.
- [ ] Document runtime-specific adapters for Tokio and WASM.

## Acceptance Criteria

- Existing sync APIs continue to compile unchanged.
- Async support is absent unless the feature is enabled.
- Async tests verify behavior parity with sync `MemStore`.
- Ordered batch reads preserve duplicate positions and result ordering.
- Broad diffs and range scans respect bounded concurrency.
- Object-store node reads verify content by CID.
- Hints remain optional and never affect correctness.
- The async API does not require Tokio unless a Tokio-specific backend is used.

## Risks and Open Questions

- Avoiding algorithm duplication between `Prolly` and `AsyncProlly`.
- Choosing between `async fn` in traits, associated future types, or boxed
  futures.
- Supporting non-`Send` browser futures without weakening native concurrency.
- Defining root manifest consistency for eventually consistent object stores.
- Avoiding accidental unbounded task creation during diff and merge.
- Deciding whether async support should live in this crate or a companion crate.