diff --git a/README.md b/README.md index 602ccc6c..aba30282 100644 --- a/README.md +++ b/README.md @@ -237,41 +237,23 @@ public class ItemData : DatabaseEntry, INamedEntry - backing stores are persistence adapters - indexes and name maps are maintained inside the cache, not inside the store -### Important: Multiple Backing Stores +### Multiple Backing Stores -When multiple backing stores are added, behavior depends on how they are registered. - -If a store is added with domain types: +Every document type has exactly one home store. There are no mirrors. ```csharp -cache.AddBackingStore(playerStore, typeof(PlayerData)); -cache.AddBackingStore(settingsStore, typeof(AppSettings)); +cache.AddBackingStore(catalogStore, typeof(ItemData), typeof(Faction)); +cache.AddBackingStore(runStore, typeof(SavedGame)); ``` -then that store becomes the direct persistence target for those types. +- the home is chosen by the document's runtime type: the most derived routed type wins, otherwise the untyped store +- at most one untyped store; a type claimed by two stores is an error +- a write with no home is an error once any store is attached +- attaching a store reads it; attach routed stores first and the untyped store (if any) last, because a record's home cannot move after it is loaded +- loading never writes: opening and flushing without a mutation leaves every file byte-identical +- a read-only store refuses writes and is skipped by flush -If a store is added without domain types: - -```csharp -cache.AddBackingStore(primaryStore); -cache.AddBackingStore(mirrorStore); -``` - -then the first generic store acts as the primary writable store for non-domain-specific entries. Additional generic stores subscribe to the existing stores and mirror their change events. - -Implications: - -- order matters for generic stores -- `AddAsync` writes to the type-specific store when one exists -- otherwise `AddAsync` writes to the first generic store -- later generic stores do not become co-primaries; they mirror earlier stores -- `PullAllBackingStoresAsync` pulls from every registered store - -Recommended patterns: - -- use one generic primary store if you want simple persistence -- use domain-specific stores when different entry types belong in different persistence layers -- treat additional generic stores as mirrors or downstream replicas, not independent write targets +The contract is `src/GameCult.Caching/Contracts/cultcache-store-composition.md`. Until the C# cache implements it, `AddBackingStore` takes no home types and every attached store receives every record; attach one store per cache. ## Example: Cache + Networking diff --git a/docs/runtime-parity-scope.md b/docs/runtime-parity-scope.md index 5d4f6b50..515a8428 100644 --- a/docs/runtime-parity-scope.md +++ b/docs/runtime-parity-scope.md @@ -21,16 +21,17 @@ Cross-runtime parity has three layers: transport helpers instead of assembling raw maps and channel strings. SoA performance parity is deliberately not a cross-runtime requirement. C# -owns the CPU-local SoA table and shared-memory streaming body path. Other -runtimes may add native hot paths when their runtime role earns them, but they -do not need to mimic Unity/C# memory layout to speak CultNet or participate in -CultMesh. +owns the shared-memory streaming body path. The C# CultCache SoA table is not a +supported surface: nothing outside tests consumes it, and it is being parked at +a tag rather than carried. Other runtimes may add native hot paths when their +runtime role earns them, but they do not need to mimic Unity/C# memory layout +to speak CultNet or participate in CultMesh. ## Current Targeted Runtimes | Runtime | Role | Claimed parity | Not claimed | | --- | --- | --- | --- | -| C# | Reference implementation and production game/server substrate | CultCache typed documents and SoA, CultNet schema-v0, LiteNetLib production path with adapter profile, shared transport profiles, shared RUDP socket transport, C# multi-peer RUDP listener and schema service host, C# interop RUDP serve/dial path, connected RUDP CultMesh client helpers, RUDP-backed schema-client selection for `rudp://` service endpoints, RUDP reconnect policy/controller and reconnect-loop helper, LiteNetLib client reconnect-controller adoption, schema-client transport port for service request/response clients, CultMesh node/session/catalog/stream/simulation/RUDP surfaces, authorized peer lookup | No need to abandon LiteNetLib before the CultNet adapter fully owns production service adoption | +| C# | Reference implementation and production game/server substrate | CultCache typed documents, CultNet schema-v0, LiteNetLib production path with adapter profile, shared transport profiles, shared RUDP socket transport, C# multi-peer RUDP listener and schema service host, C# interop RUDP serve/dial path, connected RUDP CultMesh client helpers, RUDP-backed schema-client selection for `rudp://` service endpoints, RUDP reconnect policy/controller and reconnect-loop helper, LiteNetLib client reconnect-controller adoption, schema-client transport port for service request/response clients, CultMesh node/session/catalog/stream/simulation/RUDP surfaces, authorized peer lookup | No need to abandon LiteNetLib before the CultNet adapter fully owns production service adoption | | Python | Package runtime, local daemon endpoint, raw interop peer, typed state participant | CultCache/CultNet/CultMesh package surface, branded CultMesh node/schema/shard/Verse/peer/authority/stream/RUDP ergonomics, authorized peer lookup, shard catalogs/logs, schema-transport factory for raw client request/subscription flows, local TCP and RUDP CultMesh server listeners, Python interop RUDP serve/dial path, session/prediction/simulation fact helpers, RUDP codec/session/socket transport, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, benchmark evidence against C# cache paths | Universal C# throughput, full C# daemon/server stack, and RUDP as the only public service pipe | | TypeScript | Node/browser-adjacent projection and interop runtime | CultCache parity tests, CultNet schema-v0 peer, advertised schema/shard catalog wire contracts plus request/sync helpers, transport profiles, TCP-framed transport port and peer helper, shared RUDP socket transport, RUDP interop serve/dial path, RUDP reconnect policy/controller, RUDP reconnect-loop helper, branded CultMesh node/schema/shard/Verse/peer/authority/stream/RUDP ergonomics, authorized peer lookup | Full C# game session/server stack | | Rust | Low-level cache/transport/runtime substrate | CultCache/CultNet interop lane, schema and shard catalog helpers, typed same-schema alias hydration from raw snapshots, transport profiles, TCP-framed port, shared RUDP codec/session/socket transport, Rust interop RUDP serve/dial path, connected RUDP CultMesh client helpers, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, small CultMesh facade for schema registry, shard catalog, peer cards, authority leases, authorized peer lookup, RUDP endpoint parsing, and peer-card/authorized-peer RUDP client construction | Full CultMesh game-session/server facade or production game-session server. `packages/cultmesh-rs` now holds a Rust stream and session surface -- node, document sets, RUDP document publication, stream descriptors, negotiation, stream catalog, and shared-memory frame rings -- but no interop lane exercises it, so it carries no cross-runtime parity claim | diff --git a/packages/cultcache-rs/README.md b/packages/cultcache-rs/README.md index 0176f474..078a108f 100644 --- a/packages/cultcache-rs/README.md +++ b/packages/cultcache-rs/README.md @@ -230,15 +230,17 @@ cache.add_backing_store( ); ``` -When writing a `PlayerData`, the cache checks type-specific stores first. If none -match, it writes to the first generic store. Later matching stores are mirrors. +The contract, shared with every runtime, is one home store per document type +(`src/GameCult.Caching/Contracts/cultcache-store-composition.md` in CultLib): -This mirrors the C# behavior: +- 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 write touches exactly one store; there are no mirrors -- specific domain stores own their domain -- the first generic store is the primary generic write target -- later generic stores mirror writes -- this is not multi-master +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. ## Persistence Semantics diff --git a/src/GameCult.Caching.MessagePack.Generator/README.md b/src/GameCult.Caching.MessagePack.Generator/README.md index 79bfd29a..92acbcb0 100644 --- a/src/GameCult.Caching.MessagePack.Generator/README.md +++ b/src/GameCult.Caching.MessagePack.Generator/README.md @@ -1,73 +1,52 @@ # GameCult.Caching.MessagePack.Generator -`GameCult.Caching.MessagePack.Generator` is a Roslyn incremental source generator that emits MessagePack formatters for concrete `DatabaseEntry` subclasses. +A Roslyn incremental source generator that emits CultCache document metadata, +and MessagePack payload codecs where it can, for classes marked +`[CultDocument]`. ## Scope -This project is infrastructure for the caching stack. Application code usually does not reference it directly. - -Its job is to: - -- find non-abstract subclasses of `DatabaseEntry` -- generate concrete `IMessagePackFormatter` implementations -- generate resolver mappings consumed by `DatabaseEntryResolver` +Infrastructure for the caching stack. Application code does not reference it +directly; it arrives through `GameCult.Caching.MessagePack.Analyzers`. Without +it, `CultDocumentRegistry` builds the same descriptors by reflection. ## What It Generates -For each discovered concrete `DatabaseEntry` subtype, the generator emits: +One `GeneratedCultDocumentMetadataProvider_` per assembly, registered +with `[assembly: CultGeneratedDocumentMetadataProvider]`, holding for each +`[CultDocument]` class: -- a formatter type -- a resolver mapping entry +- schema name, schema version, and whether the type is `[CultGlobal]` +- member definitions (name, slot, persisted type name, reference and + cardinality, `[CultName]`, `[CultIndex]`) +- name and index accessors +- a payload serializer, and a deserializer when the type has a public or + internal parameterless constructor -Member slot assignment is: +Members are the public, non-static, non-`[IgnoreMember]` fields and settable +properties **declared on the class itself**; inherited members are not +discovered. Members with `[Key(n)]` take slot `n`; unkeyed members follow the +highest key in ordinal name order. Annotate every persisted member: source +order is not a schema. -1. `ID` in slot `0` -2. members with `[Key(...)]` in their exact key slot -3. remaining non-ignored instance fields and properties appended after the - highest occupied slot as legacy convenience +Payload codecs are emitted only when slots are dense (`0..n-1`) and the +compilation references MessagePack and `GameCult.Caching.MessagePack`. +Otherwise the definition carries no codec and serialization falls back to the +reflective path. A generated payload is an array of the members in slot order; +the deserializer leaves missing trailing slots at their initializer values and +skips unknown extra slots. Generated codecs read +`CultDocumentMessagePackSerialization.Options` at call time. -The generated formatter writes a MessagePack array sized to the highest occupied -slot and writes nil for unused gaps. Deserialization tolerates shorter older -arrays by leaving missing members at their constructor/field-initializer values, -and skips unknown extra slots. That makes `[Key(n)]` a durable wire contract: -deleted fields can leave their slot reserved and new fields can use new keys -without a store migration. - -For schema-stable cache entries, annotate every persisted member. Unkeyed -members are supported for older code, but they are not a good long-term wire -contract because source member order is not a schema. - -## Example - -Given: - -```csharp -[MessagePackObject] -public class PlayerData : DatabaseEntry, INamedEntry -{ - [Key(1)] public string Email = string.Empty; - [Key(2)] public string PasswordHash = string.Empty; - [Key(3)] public string Username = string.Empty; - - [IgnoreMember] - public string EntryName - { - get => Username; - set => Username = value; - } -} -``` - -the generator emits the formatter plumbing needed for MessagePack serialization of `PlayerData` without manually implementing an `IMessagePackFormatter`. +The generator has no `[Union]` handling. ## When You Need To Think About This Package -Usually only when: - -- formatter generation is not happening -- a cache-entry type is not serializing as expected -- you are debugging build-time source generation behavior +- a document's descriptor or schema id differs between generated and + reflective builds +- a document serializes through the reflective path when you expected a codec +- you are debugging build-time source generation ## Distribution -The generator is packaged for consumers through [../GameCult.Caching.MessagePack.Analyzers/README.md](../GameCult.Caching.MessagePack.Analyzers/README.md). +Packaged for consumers through +[../GameCult.Caching.MessagePack.Analyzers/README.md](../GameCult.Caching.MessagePack.Analyzers/README.md).