diff --git a/contracts/cultnet/cultnet.error.schema.json b/contracts/cultnet/cultnet.error.schema.json
index 15880fa1..0d541262 100644
--- a/contracts/cultnet/cultnet.error.schema.json
+++ b/contracts/cultnet/cultnet.error.schema.json
@@ -28,7 +28,7 @@
"code": {
"description": "Machine-readable refusal code (R-N). Additive: a v0 peer that does not know this field ignores it.",
"type": ["string", "null"],
- "enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported"]
+ "enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema"]
},
"details": {
"description": "Code-specific structured detail (R-N). Every field is populated only for the code that defines it; the others are null, not omitted.",
@@ -36,8 +36,8 @@
"additionalProperties": false,
"required": ["field", "value", "asOf", "current"],
"properties": {
- "field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended.", "type": ["string", "null"] },
- "value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended.", "type": ["string", "null"] },
+ "field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended. unowned_schema: \"schemaId\".", "type": ["string", "null"] },
+ "value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended. unowned_schema: the schema id no shard owns.", "type": ["string", "null"] },
"asOf": { "description": "cursor_stale: the cursor's own minted asOf.", "type": ["integer", "null"], "minimum": 0 },
"current": { "description": "cursor_stale: the answering server's current asOf.", "type": ["integer", "null"], "minimum": 0 }
}
diff --git a/packages/cultnet-rs/contracts/cultnet/cultnet.error.schema.json b/packages/cultnet-rs/contracts/cultnet/cultnet.error.schema.json
index 15880fa1..0d541262 100644
--- a/packages/cultnet-rs/contracts/cultnet/cultnet.error.schema.json
+++ b/packages/cultnet-rs/contracts/cultnet/cultnet.error.schema.json
@@ -28,7 +28,7 @@
"code": {
"description": "Machine-readable refusal code (R-N). Additive: a v0 peer that does not know this field ignores it.",
"type": ["string", "null"],
- "enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported"]
+ "enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema"]
},
"details": {
"description": "Code-specific structured detail (R-N). Every field is populated only for the code that defines it; the others are null, not omitted.",
@@ -36,8 +36,8 @@
"additionalProperties": false,
"required": ["field", "value", "asOf", "current"],
"properties": {
- "field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended.", "type": ["string", "null"] },
- "value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended.", "type": ["string", "null"] },
+ "field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended. unowned_schema: \"schemaId\".", "type": ["string", "null"] },
+ "value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended. unowned_schema: the schema id no shard owns.", "type": ["string", "null"] },
"asOf": { "description": "cursor_stale: the cursor's own minted asOf.", "type": ["integer", "null"], "minimum": 0 },
"current": { "description": "cursor_stale: the answering server's current asOf.", "type": ["integer", "null"], "minimum": 0 }
}
diff --git a/packages/cultnet-rs/src/contracts.rs b/packages/cultnet-rs/src/contracts.rs
index f31ade85..1808c62f 100644
--- a/packages/cultnet-rs/src/contracts.rs
+++ b/packages/cultnet-rs/src/contracts.rs
@@ -250,8 +250,8 @@ impl CultNetShardDescriptor {
}
}
-/// Machine-readable `cultnet.error.v0` refusal code (R-N). Mirrors the five strings
-/// `contracts/cultnet/cultnet.error.schema.json` enumerates for `code`; a peer that receives a fifth
+/// Machine-readable `cultnet.error.v0` refusal code (R-N). Mirrors the strings
+/// `contracts/cultnet/cultnet.error.schema.json` enumerates for `code`; a peer that receives a
/// string it does not know refuses to decode rather than guess a meaning for it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -261,6 +261,7 @@ pub enum CultNetErrorCode {
CursorInvalid,
ReferenceOutsideTarget,
VariantUnsupported,
+ UnownedSchema,
}
impl CultNetErrorCode {
@@ -271,6 +272,7 @@ impl CultNetErrorCode {
Self::CursorInvalid => "cursor_invalid",
Self::ReferenceOutsideTarget => "reference_outside_target",
Self::VariantUnsupported => "variant_unsupported",
+ Self::UnownedSchema => "unowned_schema",
}
}
@@ -281,6 +283,7 @@ impl CultNetErrorCode {
"cursor_invalid" => Ok(Self::CursorInvalid),
"reference_outside_target" => Ok(Self::ReferenceOutsideTarget),
"variant_unsupported" => Ok(Self::VariantUnsupported),
+ "unowned_schema" => Ok(Self::UnownedSchema),
other => Err(anyhow!("ErrorMessage.Code {other:?} is not a recognized cultnet.error.v0 code")),
}
}
diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs
index 68fbfb1b..6365261f 100644
--- a/src/GameCult.Caching/CultCache.cs
+++ b/src/GameCult.Caching/CultCache.cs
@@ -2124,10 +2124,15 @@ namespace GameCult.Caching
Remove(handle.Key);
}
- // false: a condition failed; nothing was written, changed in memory, or published. A commit that succeeds and then
- // fails in a journal or observer still stands and is published; the writer receives the failure. One failure arrives
- // as itself, several as an AggregateException. A writer that wants a particular failure (CultNetShardLogException, when
- // a database's log refused the commit) catches that type, or unwraps AggregateException.InnerExceptions.
+ ///
+ /// Commits the staged batch. Returns false when a condition failed: nothing was written, changed in memory, or published.
+ ///
+ ///
+ /// A commit that succeeds and then fails in a journal or observer still stands and is published, and the writer
+ /// receives the failure. One failure arrives as itself, several as an . A writer that
+ /// wants a particular failure (a CultNetShardLogException, when a database's log refused the commit) catches that type,
+ /// or unwraps and looks for it.
+ ///
public bool Commit(Action stage)
{
return Land(stage, wait: true) == CultCommitOutcome.Committed;
diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs
index 52c67482..bdc1dbdc 100644
--- a/src/GameCult.Mesh/CultMesh.cs
+++ b/src/GameCult.Mesh/CultMesh.cs
@@ -3370,6 +3370,20 @@ namespace GameCult.Mesh
$"CultMesh document '{key.Value}' was not found as {typeof(TDocument).FullName}.");
}
+ // A database serves only what its shards own; the cache behind it may hold more.
+ private static bool DatabaseServes(CultNetDatabase database, object document, CultRecordKey key)
+ {
+ try
+ {
+ database.ResolveShard(CultDocumentRegistry.Shared.GetRequired(document.GetType()).SchemaId, key);
+ return true;
+ }
+ catch (CultNetUnownedSchemaException)
+ {
+ return false;
+ }
+ }
+
private static async Task ReadDatabaseDocumentRequiredAsync(
CultNetDatabase database,
CultRecordKey key)
@@ -3380,7 +3394,7 @@ namespace GameCult.Mesh
return document;
var untyped = database.Cache.Get(key);
- if (untyped != null && IsSameCultDocumentSchema(untyped.GetType()))
+ if (untyped != null && IsSameCultDocumentSchema(untyped.GetType()) && DatabaseServes(database, untyped, key))
return ConvertUntypedDocument(untyped);
throw new KeyNotFoundException(
diff --git a/src/GameCult.Networking/CultNetDatabase.cs b/src/GameCult.Networking/CultNetDatabase.cs
index 07336488..1cb1a1e0 100644
--- a/src/GameCult.Networking/CultNetDatabase.cs
+++ b/src/GameCult.Networking/CultNetDatabase.cs
@@ -920,16 +920,18 @@ namespace GameCult.Networking
public Task GetAsync(CultRecordKey key) where T : class
{
ThrowIfDisposed();
- return Task.FromResult(_cache.Get(key));
+ var document = _cache.Get(key);
+ return Task.FromResult(document != null && Owns(_cache.Registry.GetRequired(document.GetType()), key) ? document : null);
}
///
- /// Gets all documents assignable to the requested type.
+ /// Gets all documents assignable to the requested type. A database serves only what its shards own: a cached row no
+ /// shard owns is left out here, as it is from every read and projection. The cache itself still holds it.
///
public IEnumerable GetAll() where T : class
{
ThrowIfDisposed();
- return _cache.GetAll();
+ return _cache.GetAll().Where(Serves);
}
///
@@ -938,7 +940,8 @@ namespace GameCult.Networking
public T? GetByName(string name) where T : class
{
ThrowIfDisposed();
- return _cache.GetByName(name);
+ var document = _cache.GetByName(name);
+ return document != null && Serves(document) ? document : null;
}
///
@@ -947,12 +950,15 @@ namespace GameCult.Networking
public T? GetByIndex(string alias, string value) where T : class
{
ThrowIfDisposed();
- return _cache.GetByIndex(alias, value);
+ var document = _cache.GetByIndex(alias, value);
+ return document != null && Serves(document) ? document : null;
}
///
/// Adds or replaces a document at a specific key.
///
+ /// No shard owns the schema and key; nothing was written.
+ /// The change committed and was published, but the shard log could not record it.
public async Task> PutAsync(CultRecordKey key, T document) where T : class
{
ThrowIfDisposed();
@@ -967,6 +973,7 @@ namespace GameCult.Networking
///
/// Applies a locally predicted document for a client-owned input scope.
///
+ /// No shard owns the schema and key; nothing was written.
public async Task> PutPredictedAsync(CultRecordKey key, T document) where T : class
{
ThrowIfDisposed();
@@ -991,6 +998,8 @@ namespace GameCult.Networking
///
/// Deletes a document by key.
///
+ /// No shard owns the schema and key; nothing was written.
+ /// The change committed and was published, but the shard log could not record it.
public Task DeleteAsync(CultRecordKey key) where T : class
{
ThrowIfDisposed();
@@ -1004,6 +1013,8 @@ namespace GameCult.Networking
///
/// Applies a raw document mutation after checking shard authority.
///
+ /// No shard owns the schema and key; nothing was written.
+ /// The change committed and was published, but the shard log could not record it.
public async Task