spacedb-crdt 0.5.0

SpaceDB Layer 1 — convergent collections: the Y-CRDT (yrs) document model with a typed field->CRDT mapping (LWW-Register, PN-Counter, ...), local-first mutation, and order-independent merge. Phase 1 SpaceDB Mission, M2. See 'docs/plans/(working) phase 1 spacedb mission.md'.
Documentation
# spacedb-crdt

**SpaceDB Layer 1 — convergent collections.**

The default data tier, and the reason SpaceDB survives a partition-prone mesh:
data is modeled as **Y-CRDT** (via [`yrs`](https://crates.io/crates/yrs)), so
every write is locally available and merges conflict-free with no coordination.
Writing offline on a Starlink-partitioned home is a non-event, not an error.

Part of [SpaceDB](../README.md). Dual-licensed **MIT OR Apache-2.0**.

```toml
[dependencies]
spacedb-crdt = "0.5"
```

## The model

A `CrdtDoc` is a document with a typed field → CRDT-type mapping. Pick the CRDT
per field; the merge rule follows from the type.

```rust
use spacedb_crdt::CrdtDoc;

let laptop = CrdtDoc::new(1);   // actor id
let phone  = CrdtDoc::new(2);

laptop.set_register("display_name", &"Ada").unwrap();  // LWW-Register
laptop.increment("visits", 1);                          // PN-Counter
laptop.text_push("bio", "building on SpaceDB");         // Y.Text
laptop.set_add("tags", "rust");                         // OR-Set

// Sync: exchange state vectors, ship only the delta they imply.
let delta = laptop.encode_update_since(&phone.state_vector()).unwrap();
phone.apply_update(&delta).unwrap();

assert_eq!(phone.get_register::<String>("display_name").unwrap().as_deref(), Some("Ada"));
assert_eq!(phone.counter("visits"), 1);
```

| CRDT type | Field API | Merge rule |
|---|---|---|
| LWW-Register | `set_register` / `get_register` / `remove_register` | last writer wins |
| PN-Counter | `increment` / `counter` | sum of per-actor increments |
| Y.Text | `text_push` / `text_insert` / `text_remove` / `text` | character-level intent preservation |
| OR-Set | `set_add` / `set_remove` / `set_contains` / `set_members` | add wins over concurrent remove |

## Reactive queries

```rust
use spacedb_crdt::CrdtDoc;
let doc = CrdtDoc::new(1);
let watcher = doc.watch();
// ... after any local or merged write:
if watcher.drain_changed() { /* re-render */ }
```

`ReactiveQuery::poll` gives the same thing with a derived value: it recomputes
only when the document revision moves.

## Encrypted persistence

`CrdtStore` writes documents into a [`spacedb-store`](../spacedb-store/) engine
behind the `KeyProvider` AEAD boundary — `save`, `load`, `apply_remote`,
`contains`. `compact_updates` folds an update log into one minimal update so a
long-lived document doesn't grow without bound.

## The convergence property

*The same updates, applied in any order, produce the same state.* That is the
guarantee everything above this layer depends on, and it is proven — not
asserted — by the fuzzed suite in [`tests/convergence.rs`](tests/convergence.rs),
which shuffles update orderings across replicas and requires byte-identical
final state.

`ops_behind` and `estimated_state_size` expose honest lag / size, which
[`spacedb-replica`](../spacedb-replica/) turns into a `Freshness` verdict.

## Open-core boundary

Depends on `yrs` and `spacedb-store` — never on a MATA crate.

## Testing

The workspace defaults to `wasm32`; this crate is native. Test on your host
triple:

```bash
cargo test -p spacedb-crdt --target aarch64-apple-darwin   # or your host triple
```

Suites: `convergence.rs` (fuzzed), `fields.rs`, `reactive.rs`, `persistence.rs`,
`compact.rs`, `lag.rs`.

## License

MIT OR Apache-2.0, at your option. See [LICENSE-MIT](../LICENSE-MIT) and
[LICENSE-APACHE](../LICENSE-APACHE).