Correct stale store-routing, SoA, and generator docs
The README and the Rust README described mirrors as C# behavior; the parity scope claimed the C# SoA table; the generator README described DatabaseEntry formatters that no longer exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
37f73c4e38
commit
486fe560af
4 changed files with 64 additions and 100 deletions
40
README.md
40
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<T>` 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_<Assembly>` 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<PlayerData>`.
|
||||
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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue