DocStore
Public store implementation providing document-oriented storage with path-based nested data access.
Overview
DocStore is a publicly available store type that provides a document-oriented interface for storing and retrieving data. It wraps the crdt::Doc
type to provide ergonomic access patterns for nested data structures, making it ideal for configuration, metadata, and structured document storage.
Key Characteristics
Public API: DocStore is exposed as part of the public store API and can be used in applications.
Doc CRDT Based: Wraps the crdt::Doc
type which uses Node structures internally for deterministic merging of concurrent changes.
Path-Based Operations: Supports both flat key-value storage and path-based access to nested structures.
Important Behavior: Nested Structure Creation
Path-Based Operations Create Nested Maps
When using set_path()
with dot-separated paths, DocStore creates nested map structures, not flat keys with dots:
// This code:
docstore.set_path("user.profile.name", "Alice")?;
// Creates this structure:
{
"user": {
"profile": {
"name": "Alice"
}
}
}
// NOT this:
{ "user.profile.name": "Alice" } // ❌ This is NOT what happens
Accessing Nested Data
When using get_all()
to retrieve all data, you get the nested structure and must navigate it accordingly:
let all_data = docstore.get_all()?;
// Wrong way - looking for a flat key with dots
let value = all_data.get("user.profile.name"); // ❌ Returns None
// Correct way - navigate the nested structure
if let Some(Value::Node(user_node)) = all_data.get("user") {
if let Some(Value::Node(profile_node)) = user_node.get("profile") {
if let Some(Value::Text(name)) = profile_node.get("name") {
println!("Name: {}", name); // ✅ "Alice"
}
}
}
API Methods
Basic Operations
set(key, value)
- Set a simple key-value pairget(key)
- Get a value by keyget_as<T>(key)
- Get and deserialize a valuedelete(key)
- Delete a key (creates tombstone)get_all()
- Get all data as a Map
Path Operations
set_path(path, value)
- Set a value at a nested path (creates intermediate maps)get_path(path)
- Get a value from a nested pathget_path_as<T>(path)
- Get and deserialize from a pathdelete_path(path)
- Delete a value at a path
Path Mutation Operations
modify_path<F>(path, f)
- Modify existing value at pathget_or_insert_path<F>(path, default)
- Get or insert with defaultmodify_or_insert_path<F, G>(path, modify, default)
- Modify or insert
Utility Operations
contains_key(key)
- Check if a key existscontains_path(path)
- Check if a path exists
Usage Examples
Application Configuration
let op = database.new_transaction()?;
let config = op.get_subtree::<DocStore>("app_config")?;
// Set configuration values
config.set("app_name", "MyApp")?;
config.set_path("database.host", "localhost")?;
config.set_path("database.port", "5432")?;
config.set_path("features.auth.enabled", "true")?;
op.commit()?;
Sync State Management
DocStore is used internally for sync state tracking in the sync module:
// Creating nested sync state structure
let sync_state = op.get_subtree::<DocStore>("sync_state")?;
// Store cursor information in nested structure
let cursor_path = format!("cursors.{}.{}", peer_pubkey, tree_id);
sync_state.set_path(cursor_path, cursor_json)?;
// Store metadata in nested structure
let metadata_path = format!("metadata.{}", peer_pubkey);
sync_state.set_path(metadata_path, metadata_json)?;
// Store history in nested structure
let history_path = format!("history.{}", sync_id);
sync_state.set_path(history_path, history_json)?;
// Later, retrieve all data and navigate the structure
let all_data = sync_state.get_all()?;
// Navigate to history entries
if let Some(Value::Node(history_node)) = all_data.get("history") {
for (sync_id, entry_value) in history_node.iter() {
// Process each history entry
if let Value::Text(json_str) = entry_value {
let entry: SyncHistoryEntry = serde_json::from_str(json_str)?;
// Use the entry...
}
}
}
Common Pitfalls
Expecting Flat Keys
The most common mistake is expecting set_path("a.b.c", value)
to create a flat key "a.b.c"
when it actually creates nested maps.
Incorrect get_all() Usage
When using get_all()
, remember that the returned Map contains the nested structure, not flat keys:
// After: docstore.set_path("config.server.port", "8080")
let all = docstore.get_all()?;
// Wrong:
all.get("config.server.port") // Returns None
// Right:
all.get("config")
.and_then(|v| v.as_node())
.and_then(|n| n.get("server"))
.and_then(|v| v.as_node())
.and_then(|n| n.get("port")) // Returns Some(Value::Text("8080"))
Design Rationale
The nested structure approach was chosen because:
- Natural Hierarchy: Represents hierarchical data more naturally
- Partial Updates: Allows updating parts of a structure without rewriting everything
- CRDT Compatibility: Works well with Doc CRDT merge semantics
- Query Flexibility: Enables querying at any level of the hierarchy
See Also
- Doc CRDT - Underlying CRDT implementation
- Sync State Management - Primary use case for DocStore
- SubTree Trait - Base trait for all store implementations