Give every cultcache-rs entry type one home store
Cut 5 (Rust). The four mirror loops in put, put_envelope, put_raw_envelope and delete are deleted, and so is put_prepared_batch's route.len() != 1 branch. resolve_route_index returns the single home store (the store that claims the type, else the generic store) and replaces resolve_route_indices. add_backing_store and add_generic_backing_store return Result and refuse a second generic store or a type already claimed by another store, attaching nothing when they refuse. Callers take the Result: the crate's tests, the cultcache_interop example, cultnet-rs's tests and interop peer example, and CultMeshNode::create_node in cultmesh-rs. Tests: second_generic_store_is_rejected, type_claimed_twice_is_rejected; type_specific_store_routes_before_generic_store stays, now attaching the routed store before the generic one. No bytes change. The store-composition contract's runtime table now records Rust, TypeScript and Python as conforming to one home store per type, and the README routing section drops its non-conformance note. Version 0.1.0 -> 0.2.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5967a1f25d
commit
6bb41f39ea
9 changed files with 120 additions and 98 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cultcache-rs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ not a missing reflection party trick.
|
|||
Generic store:
|
||||
|
||||
```rust
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new("cache.cc"));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new("cache.cc"))?;
|
||||
```
|
||||
|
||||
Type-specific store:
|
||||
|
|
@ -227,20 +227,17 @@ Type-specific store:
|
|||
cache.add_backing_store(
|
||||
SingleFileMessagePackBackingStore::new("players.cc"),
|
||||
["player"],
|
||||
);
|
||||
)?;
|
||||
```
|
||||
|
||||
The contract, shared with every runtime, is one home store per document type
|
||||
(`src/GameCult.Caching/Contracts/cultcache-store-composition.md` in CultLib):
|
||||
|
||||
- a type-specific store owns its types; otherwise the generic store is the home
|
||||
- at most one generic store, and no type claimed by two stores
|
||||
- a second generic store is an error, and so is a type claimed by a second
|
||||
store; both registrations return `Err` and attach nothing
|
||||
- a write touches exactly one store; there are no mirrors
|
||||
|
||||
This crate does not yet enforce it: `add_backing_store` accepts overlapping
|
||||
routes and a second generic store, and writes are pushed to every matching
|
||||
store after the first. Attach one generic store, or type-specific stores whose
|
||||
types do not overlap.
|
||||
- a write whose type has no home store is an error
|
||||
|
||||
## Persistence Semantics
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ fn read_note(file: &str) -> Result<()> {
|
|||
fn build_cache(file: &str) -> Result<CultCache> {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<CultCacheInteropNote>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file))?;
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1947,19 +1947,38 @@ impl CultCache {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
/// Makes the store home to `types`, or the generic store when `types` is
|
||||
/// empty. A type has exactly one home store: a second generic store or a
|
||||
/// type already claimed by another store is an error and attaches nothing.
|
||||
pub fn add_backing_store(
|
||||
&mut self,
|
||||
store: impl CacheBackingStore + 'static,
|
||||
types: impl IntoIterator<Item = impl Into<String>>,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let types: BTreeSet<String> = types.into_iter().map(Into::into).collect();
|
||||
if types.is_empty() && self.stores.iter().any(|registration| registration.types.is_empty()) {
|
||||
return Err(anyhow!(
|
||||
"Backing store would be a second generic store; name the types it is home to"
|
||||
));
|
||||
}
|
||||
if let Some(claimed) = types
|
||||
.iter()
|
||||
.find(|type_id| self.stores.iter().any(|registration| registration.types.contains(*type_id)))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"CultCache type {:?} is already routed to another backing store; it cannot also route to this one",
|
||||
claimed
|
||||
));
|
||||
}
|
||||
self.stores.push(CultCacheStoreRegistration {
|
||||
store: Box::new(store),
|
||||
types: types.into_iter().map(Into::into).collect(),
|
||||
types,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_generic_backing_store(&mut self, store: impl CacheBackingStore + 'static) {
|
||||
self.add_backing_store(store, Vec::<String>::new());
|
||||
pub fn add_generic_backing_store(&mut self, store: impl CacheBackingStore + 'static) -> Result<()> {
|
||||
self.add_backing_store(store, Vec::<String>::new())
|
||||
}
|
||||
|
||||
pub fn pull_all_backing_stores(&mut self) -> Result<()> {
|
||||
|
|
@ -2081,17 +2100,13 @@ impl CultCache {
|
|||
|
||||
pub fn put<T: DatabaseEntry>(&mut self, key: impl Into<String>, value: &T) -> Result<T> {
|
||||
let (entry, parsed) = self.prepare_entry(key, value)?;
|
||||
let route = self.resolve_route_indices(T::TYPE);
|
||||
let Some(primary_index) = route.first().copied() else {
|
||||
let Some(home) = self.resolve_route_index(T::TYPE) else {
|
||||
return Err(anyhow!(
|
||||
"No backing store is registered for entry type {:?}",
|
||||
T::TYPE
|
||||
));
|
||||
};
|
||||
self.stores[primary_index].store.push(&entry)?;
|
||||
for mirror_index in route.iter().skip(1).copied() {
|
||||
self.stores[mirror_index].store.push(&entry)?;
|
||||
}
|
||||
self.stores[home].store.push(&entry)?;
|
||||
self.entries.insert(entry_id(&entry), entry);
|
||||
Ok(parsed)
|
||||
}
|
||||
|
|
@ -2171,19 +2186,17 @@ impl CultCache {
|
|||
entry.r#type
|
||||
));
|
||||
}
|
||||
let route = self.resolve_route_indices(&entry.r#type);
|
||||
if route.len() != 1 {
|
||||
let Some(home) = self.resolve_route_index(&entry.r#type) else {
|
||||
return Err(anyhow!(
|
||||
"CultCache atomic batch requires exactly one backing-store route for type {:?}, found {}",
|
||||
entry.r#type,
|
||||
route.len()
|
||||
"No backing store is registered for entry type {:?}",
|
||||
entry.r#type
|
||||
));
|
||||
}
|
||||
};
|
||||
match store_index {
|
||||
Some(expected) if expected != route[0] => {
|
||||
Some(expected) if expected != home => {
|
||||
return Err(anyhow!("CultCache atomic batch cannot span backing stores"));
|
||||
}
|
||||
None => store_index = Some(route[0]),
|
||||
None => store_index = Some(home),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -2202,17 +2215,13 @@ impl CultCache {
|
|||
|
||||
pub fn put_envelope<T: DatabaseEntry>(&mut self, entry: CultCacheEnvelope) -> Result<T> {
|
||||
let parsed = self.validate_envelope::<T>(&entry)?;
|
||||
let route = self.resolve_route_indices(T::TYPE);
|
||||
let Some(primary_index) = route.first().copied() else {
|
||||
let Some(home) = self.resolve_route_index(T::TYPE) else {
|
||||
return Err(anyhow!(
|
||||
"No backing store is registered for entry type {:?}",
|
||||
T::TYPE
|
||||
));
|
||||
};
|
||||
self.stores[primary_index].store.push(&entry)?;
|
||||
for mirror_index in route.iter().skip(1).copied() {
|
||||
self.stores[mirror_index].store.push(&entry)?;
|
||||
}
|
||||
self.stores[home].store.push(&entry)?;
|
||||
self.entries.insert(entry_id(&entry), entry);
|
||||
Ok(parsed)
|
||||
}
|
||||
|
|
@ -2241,17 +2250,13 @@ impl CultCache {
|
|||
entry.r#type
|
||||
));
|
||||
}
|
||||
let route = self.resolve_route_indices(&entry.r#type);
|
||||
let Some(primary_index) = route.first().copied() else {
|
||||
let Some(home) = self.resolve_route_index(&entry.r#type) else {
|
||||
return Err(anyhow!(
|
||||
"No backing store is registered for entry type {:?}",
|
||||
entry.r#type
|
||||
));
|
||||
};
|
||||
self.stores[primary_index].store.push(&entry)?;
|
||||
for mirror_index in route.iter().skip(1).copied() {
|
||||
self.stores[mirror_index].store.push(&entry)?;
|
||||
}
|
||||
self.stores[home].store.push(&entry)?;
|
||||
self.entries.insert(entry_id(&entry), entry);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -2312,17 +2317,13 @@ impl CultCache {
|
|||
let Some(entry) = self.entries.get(&id).cloned() else {
|
||||
return Ok(false);
|
||||
};
|
||||
let route = self.resolve_route_indices(T::TYPE);
|
||||
let Some(primary_index) = route.first().copied() else {
|
||||
let Some(home) = self.resolve_route_index(T::TYPE) else {
|
||||
return Err(anyhow!(
|
||||
"No backing store is registered for entry type {:?}",
|
||||
T::TYPE
|
||||
));
|
||||
};
|
||||
self.stores[primary_index].store.delete(&entry)?;
|
||||
for mirror_index in route.iter().skip(1).copied() {
|
||||
self.stores[mirror_index].store.delete(&entry)?;
|
||||
}
|
||||
self.stores[home].store.delete(&entry)?;
|
||||
self.entries.remove(&id);
|
||||
Ok(true)
|
||||
}
|
||||
|
|
@ -2360,23 +2361,17 @@ impl CultCache {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_route_indices(&self, type_id: &str) -> Vec<usize> {
|
||||
let type_specific: Vec<usize> = self
|
||||
.stores
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, registration)| {
|
||||
registration.types.contains(type_id).then_some(index)
|
||||
})
|
||||
.collect();
|
||||
if !type_specific.is_empty() {
|
||||
return type_specific;
|
||||
}
|
||||
/// The store that claims the type, else the generic store. Registration
|
||||
/// guarantees at most one of each.
|
||||
fn resolve_route_index(&self, type_id: &str) -> Option<usize> {
|
||||
self.stores
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, registration)| registration.types.is_empty().then_some(index))
|
||||
.collect()
|
||||
.position(|registration| registration.types.contains(type_id))
|
||||
.or_else(|| {
|
||||
self.stores
|
||||
.iter()
|
||||
.position(|registration| registration.types.is_empty())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2929,14 +2924,14 @@ mod tests {
|
|||
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
cache.put("app", &settings)?;
|
||||
assert_eq!(cache.get_required::<Settings>("app")?, settings);
|
||||
|
||||
let mut reloaded = CultCache::new();
|
||||
reloaded.register_entry_type::<Settings>()?;
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
reloaded.pull_all_backing_stores()?;
|
||||
assert_eq!(reloaded.get_required::<Settings>("app")?, settings);
|
||||
Ok(())
|
||||
|
|
@ -2948,7 +2943,7 @@ mod tests {
|
|||
let store_path = temp.path().join("cache.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_registry(TestEntries)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
|
||||
cache.put(
|
||||
"shared",
|
||||
|
|
@ -2977,7 +2972,7 @@ mod tests {
|
|||
let store_path = temp.path().join("batch.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_registry(TestEntries)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
|
||||
let (settings, _) = cache.prepare_entry(
|
||||
"app",
|
||||
|
|
@ -2997,7 +2992,7 @@ mod tests {
|
|||
|
||||
let mut reloaded = CultCache::new();
|
||||
reloaded.register_registry(TestEntries)?;
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
reloaded.pull_all_backing_stores()?;
|
||||
assert_eq!(reloaded.get_required::<Settings>("app")?.theme, "iron");
|
||||
assert_eq!(reloaded.get_required::<Note>("receipt")?.title, "committed");
|
||||
|
|
@ -3010,7 +3005,7 @@ mod tests {
|
|||
let store_path = temp.path().join("named.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<NestedNote>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let note = NestedNote {
|
||||
value: NestedOptionalValue {
|
||||
|
|
@ -3683,7 +3678,7 @@ mod tests {
|
|||
cache.add_generic_backing_store(RefusingBatchStore {
|
||||
inner: SingleFileMessagePackBackingStore::new(&store_path),
|
||||
refuse_batch: refusal.clone(),
|
||||
});
|
||||
})?;
|
||||
cache.put(
|
||||
"app",
|
||||
&Settings {
|
||||
|
|
@ -3704,7 +3699,7 @@ mod tests {
|
|||
|
||||
let mut reloaded = CultCache::new();
|
||||
reloaded.register_registry(TestEntries)?;
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
reloaded.pull_all_backing_stores()?;
|
||||
assert_eq!(reloaded.get_required::<Settings>("app")?.theme, "before");
|
||||
assert!(reloaded.get::<Note>("receipt")?.is_none());
|
||||
|
|
@ -3719,11 +3714,11 @@ mod tests {
|
|||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.register_entry_type::<Note>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&generic_path));
|
||||
cache.add_backing_store(
|
||||
SingleFileMessagePackBackingStore::new(&settings_path),
|
||||
["settings"],
|
||||
);
|
||||
)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&generic_path))?;
|
||||
|
||||
cache.put(
|
||||
"app",
|
||||
|
|
@ -3747,13 +3742,44 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_generic_store_is_rejected() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let mut cache = CultCache::new();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(temp.path().join("a.cc")))?;
|
||||
let error = cache
|
||||
.add_generic_backing_store(SingleFileMessagePackBackingStore::new(temp.path().join("b.cc")))
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("second generic store"), "{error}");
|
||||
assert_eq!(cache.stores.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_claimed_twice_is_rejected() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let mut cache = CultCache::new();
|
||||
cache.add_backing_store(SingleFileMessagePackBackingStore::new(temp.path().join("a.cc")), ["settings"])?;
|
||||
let error = cache
|
||||
.add_backing_store(
|
||||
SingleFileMessagePackBackingStore::new(temp.path().join("b.cc")),
|
||||
["note", "settings"],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("already routed"), "{error}");
|
||||
// A refused registration claims nothing, so "note" is still free.
|
||||
cache.add_backing_store(SingleFileMessagePackBackingStore::new(temp.path().join("c.cc")), ["note"])?;
|
||||
assert_eq!(cache.stores.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_and_delete_follow_the_cache_api() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store_path = temp.path().join("cache.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
|
||||
let updated = cache.update::<Settings, _>("app", |current| {
|
||||
let mut current = current.unwrap_or(Settings {
|
||||
|
|
@ -3783,7 +3809,7 @@ mod tests {
|
|||
})?;
|
||||
|
||||
let mut cache = CultCache::new();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
let error = cache.pull_all_backing_stores().unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
|
|
@ -3799,7 +3825,7 @@ mod tests {
|
|||
let store_path = temp.path().join("cache.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.put(
|
||||
"app",
|
||||
&Settings {
|
||||
|
|
@ -3843,7 +3869,7 @@ mod tests {
|
|||
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let error = cache.get_required::<Settings>("app").unwrap_err();
|
||||
assert!(
|
||||
|
|
@ -3862,7 +3888,7 @@ mod tests {
|
|||
|
||||
let mut origin = CultCache::new();
|
||||
origin.register_entry_type::<Settings>()?;
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store));
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store))?;
|
||||
origin.put(
|
||||
"app",
|
||||
&Settings {
|
||||
|
|
@ -3875,7 +3901,7 @@ mod tests {
|
|||
|
||||
let mut target = CultCache::new();
|
||||
target.register_entry_type::<Settings>()?;
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?;
|
||||
let applied = target.put_envelope::<Settings>(envelope.clone())?;
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -3908,7 +3934,7 @@ mod tests {
|
|||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.register_entry_type::<Note>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.put_raw_envelope(envelope.clone())?;
|
||||
|
||||
assert_eq!(cache.snapshot(), vec![envelope.clone()]);
|
||||
|
|
@ -3950,7 +3976,7 @@ mod tests {
|
|||
let store_path = temp.path().join("cache.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_registry(TestEntries)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.put(
|
||||
"app",
|
||||
&Settings {
|
||||
|
|
@ -3974,7 +4000,7 @@ mod tests {
|
|||
restored.register_registry(TestEntries)?;
|
||||
restored.add_generic_backing_store(SingleFileMessagePackBackingStore::new(
|
||||
temp.path().join("restored.cc"),
|
||||
));
|
||||
))?;
|
||||
restored.load_soa(soa)?;
|
||||
assert_eq!(restored.get_required::<Settings>("app")?.theme, "ash");
|
||||
assert_eq!(restored.get_required::<Note>("memo")?.body, "awake");
|
||||
|
|
@ -3987,7 +4013,7 @@ mod tests {
|
|||
let store_path = temp.path().join("cache.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_registry(TestEntries)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.put(
|
||||
"settings:a",
|
||||
&Settings {
|
||||
|
|
@ -4057,7 +4083,7 @@ mod tests {
|
|||
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<SchemaStamped>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
|
||||
assert_eq!(cache.get_required::<SchemaStamped>("record-1")?, expected);
|
||||
|
|
@ -4076,7 +4102,7 @@ mod tests {
|
|||
let store_path = temp.path().join("store.cc");
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<Settings>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
cache.put(
|
||||
"primary",
|
||||
&Settings {
|
||||
|
|
@ -4115,7 +4141,7 @@ mod tests {
|
|||
|
||||
let mut reloaded = CultCache::new();
|
||||
reloaded.register_entry_type::<Settings>()?;
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path));
|
||||
reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?;
|
||||
reloaded.pull_all_backing_stores()?;
|
||||
assert_eq!(reloaded.get_required::<Settings>("primary")?.theme, "light");
|
||||
Ok(())
|
||||
|
|
|
|||
2
packages/cultmesh-rs/Cargo.lock
generated
2
packages/cultmesh-rs/Cargo.lock
generated
|
|
@ -174,7 +174,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cultcache-rs"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
|
|
|||
|
|
@ -1018,8 +1018,7 @@ impl CultMesh {
|
|||
{
|
||||
let mut cache = CultCache::new();
|
||||
documents.register_cache(&mut cache)?;
|
||||
cache
|
||||
.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref()));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref()))?;
|
||||
if options.pull_on_start {
|
||||
cache.pull_all_backing_stores()?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ fn serve(config: PeerConfig) -> Result<()> {
|
|||
register_capability_entry_types(&mut cache)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(runtime_store_path(
|
||||
&config.runtime_id,
|
||||
)));
|
||||
)))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let note = build_note(&config.runtime_id, &config.display_name);
|
||||
cache.put(¬e.document_id, ¬e)?;
|
||||
|
|
@ -380,7 +380,7 @@ fn dial(config: DialConfig) -> Result<()> {
|
|||
register_capability_entry_types(&mut cache)?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(runtime_store_path(
|
||||
&format!("{}-dial", config.runtime_id),
|
||||
)));
|
||||
)))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
|
||||
let mut document_registry = CultNetDocumentRegistry::new();
|
||||
|
|
|
|||
|
|
@ -1950,7 +1950,7 @@ fn document_registry_replicates_typed_cultcache_state() -> Result<()> {
|
|||
|
||||
let mut origin = CultCache::new();
|
||||
origin.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store));
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store))?;
|
||||
origin.pull_all_backing_stores()?;
|
||||
origin.put("epiphany.persona", &payload)?;
|
||||
|
||||
|
|
@ -1958,7 +1958,7 @@ fn document_registry_replicates_typed_cultcache_state() -> Result<()> {
|
|||
|
||||
let mut target = CultCache::new();
|
||||
target.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?;
|
||||
target.pull_all_backing_stores()?;
|
||||
let applied =
|
||||
registry.apply_snapshot_response::<GhostlightAgentStateFixture>(&mut target, &snapshot)?;
|
||||
|
|
@ -2007,7 +2007,7 @@ fn raw_snapshot_replication_preserves_messagepack_payload_bytes() -> Result<()>
|
|||
|
||||
let mut origin = CultCache::new();
|
||||
origin.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store));
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store))?;
|
||||
origin.pull_all_backing_stores()?;
|
||||
origin.put("epiphany.persona", &payload)?;
|
||||
|
||||
|
|
@ -2018,7 +2018,7 @@ fn raw_snapshot_replication_preserves_messagepack_payload_bytes() -> Result<()>
|
|||
|
||||
let mut target = CultCache::new();
|
||||
target.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?;
|
||||
target.pull_all_backing_stores()?;
|
||||
let applied = registry
|
||||
.apply_raw_snapshot_response::<GhostlightAgentStateFixture>(&mut target, &raw_snapshot)?;
|
||||
|
|
@ -2054,7 +2054,7 @@ fn raw_snapshot_replication_hydrates_same_schema_rust_aliases() -> Result<()> {
|
|||
));
|
||||
let mut origin = CultCache::new();
|
||||
origin.register_entry_type::<GhostlightAgentStateFixture>()?;
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store));
|
||||
origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store))?;
|
||||
origin.pull_all_backing_stores()?;
|
||||
origin.put("epiphany.persona", &canonical)?;
|
||||
let raw_snapshot = origin_registry.create_raw_snapshot_response(
|
||||
|
|
@ -2073,7 +2073,7 @@ fn raw_snapshot_replication_hydrates_same_schema_rust_aliases() -> Result<()> {
|
|||
));
|
||||
let mut target = CultCache::new();
|
||||
target.register_entry_type::<GhostlightAgentStateUiFixture>()?;
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?;
|
||||
target.pull_all_backing_stores()?;
|
||||
|
||||
let applied = alias_registry
|
||||
|
|
@ -2113,7 +2113,7 @@ fn reactive_document_coalesces_direct_same_schema_alias_member_writes() -> Resul
|
|||
{
|
||||
let mut cache = cache.lock().expect("cache mutex");
|
||||
cache.register_entry_type::<GhostlightReactiveNoteUiFixture>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
cache.put(
|
||||
"note:reactive",
|
||||
|
|
@ -2178,7 +2178,7 @@ fn reactive_document_tracks_canonical_reconciliation_delta() -> Result<()> {
|
|||
{
|
||||
let mut cache = cache.lock().expect("cache mutex");
|
||||
cache.register_entry_type::<GhostlightReactiveNoteFixture>()?;
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
cache.put(
|
||||
"note:reconcile",
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ no serialization; a CultMath encoding, if added, uses this shape.
|
|||
|
||||
| Rule | C# | Rust | TypeScript | Python |
|
||||
|---|---|---|---|---|
|
||||
| One home store, no mirrors | implemented | routes by type, still pushes to later matching stores | routes by type, still mirrors | routes by type, still writes every matching store |
|
||||
| One home store, no mirrors | implemented | implemented (exact type ids; `add_backing_store` returns `Err` on a second generic store or a claimed type) | implemented (exact type ids; `addBackingStore` throws on a second generic store or a claimed type) | implemented (exact type ids; `add_backing_store`/`add_generic_store` raise on a claimed type or a second generic store) |
|
||||
| Attach hydrates; loading never writes | implemented | attach does not read; pull loads without writing | attach does not read; pull loads without writing | attach does not read; pull loads without writing |
|
||||
| Runtime type decides schema | implemented | n/a (explicit type ids) | n/a | n/a |
|
||||
| Assignable lookups and watches | implemented | exact type ids | exact type ids | exact type ids |
|
||||
|
|
|
|||
Loading…
Reference in a new issue