diff --git a/packages/cultcache-rs/Cargo.toml b/packages/cultcache-rs/Cargo.toml index 6ca593bc..23d53801 100644 --- a/packages/cultcache-rs/Cargo.toml +++ b/packages/cultcache-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cultcache-rs" -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "MIT" publish = false diff --git a/packages/cultcache-rs/README.md b/packages/cultcache-rs/README.md index 078a108f..1ad01eb0 100644 --- a/packages/cultcache-rs/README.md +++ b/packages/cultcache-rs/README.md @@ -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 diff --git a/packages/cultcache-rs/examples/cultcache_interop.rs b/packages/cultcache-rs/examples/cultcache_interop.rs index 4049acf8..e625db9e 100644 --- a/packages/cultcache-rs/examples/cultcache_interop.rs +++ b/packages/cultcache-rs/examples/cultcache_interop.rs @@ -74,7 +74,7 @@ fn read_note(file: &str) -> Result<()> { fn build_cache(file: &str) -> Result { let mut cache = CultCache::new(); cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file)); + cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file))?; Ok(cache) } diff --git a/packages/cultcache-rs/src/lib.rs b/packages/cultcache-rs/src/lib.rs index 36953620..dd959a4c 100644 --- a/packages/cultcache-rs/src/lib.rs +++ b/packages/cultcache-rs/src/lib.rs @@ -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>, - ) { + ) -> Result<()> { + let types: BTreeSet = 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::::new()); + pub fn add_generic_backing_store(&mut self, store: impl CacheBackingStore + 'static) -> Result<()> { + self.add_backing_store(store, Vec::::new()) } pub fn pull_all_backing_stores(&mut self) -> Result<()> { @@ -2081,17 +2100,13 @@ impl CultCache { pub fn put(&mut self, key: impl Into, value: &T) -> Result { 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(&mut self, entry: CultCacheEnvelope) -> Result { let parsed = self.validate_envelope::(&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 { - let type_specific: Vec = 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 { 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::()?; - 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::("app")?, settings); let mut reloaded = CultCache::new(); reloaded.register_entry_type::()?; - 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::("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::("app")?.theme, "iron"); assert_eq!(reloaded.get_required::("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::()?; - 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::("app")?.theme, "before"); assert!(reloaded.get::("receipt")?.is_none()); @@ -3719,11 +3714,11 @@ mod tests { let mut cache = CultCache::new(); cache.register_entry_type::()?; cache.register_entry_type::()?; - 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::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); + cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path))?; let updated = cache.update::("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::()?; - 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::()?; - 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::("app").unwrap_err(); assert!( @@ -3862,7 +3888,7 @@ mod tests { let mut origin = CultCache::new(); origin.register_entry_type::()?; - 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::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); + target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store))?; let applied = target.put_envelope::(envelope.clone())?; assert_eq!( @@ -3908,7 +3934,7 @@ mod tests { let mut cache = CultCache::new(); cache.register_entry_type::()?; cache.register_entry_type::()?; - 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::("app")?.theme, "ash"); assert_eq!(restored.get_required::("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::()?; - 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::("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::()?; - 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::()?; - 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::("primary")?.theme, "light"); Ok(()) diff --git a/packages/cultmesh-rs/Cargo.lock b/packages/cultmesh-rs/Cargo.lock index c515ded2..945bc65b 100644 --- a/packages/cultmesh-rs/Cargo.lock +++ b/packages/cultmesh-rs/Cargo.lock @@ -174,7 +174,7 @@ dependencies = [ [[package]] name = "cultcache-rs" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "chrono", diff --git a/packages/cultmesh-rs/src/lib.rs b/packages/cultmesh-rs/src/lib.rs index 135314bb..8cfe8cec 100644 --- a/packages/cultmesh-rs/src/lib.rs +++ b/packages/cultmesh-rs/src/lib.rs @@ -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()?; } diff --git a/packages/cultnet-rs/examples/cultnet_interop_peer.rs b/packages/cultnet-rs/examples/cultnet_interop_peer.rs index 562756b8..07f6f400 100644 --- a/packages/cultnet-rs/examples/cultnet_interop_peer.rs +++ b/packages/cultnet-rs/examples/cultnet_interop_peer.rs @@ -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(); diff --git a/packages/cultnet-rs/tests/cultnet.rs b/packages/cultnet-rs/tests/cultnet.rs index b3adc7f4..0b76990f 100644 --- a/packages/cultnet-rs/tests/cultnet.rs +++ b/packages/cultnet-rs/tests/cultnet.rs @@ -1950,7 +1950,7 @@ fn document_registry_replicates_typed_cultcache_state() -> Result<()> { let mut origin = CultCache::new(); origin.register_entry_type::()?; - 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::()?; - 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::(&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::()?; - 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::()?; - 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::(&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::()?; - 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::()?; - 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::()?; - 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::()?; - 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", diff --git a/src/GameCult.Caching/Contracts/cultcache-store-composition.md b/src/GameCult.Caching/Contracts/cultcache-store-composition.md index 697bf959..e783486e 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -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 |