Make runtime catalog a typed cached authority

This commit is contained in:
Meta Vens 2026-07-20 00:39:09 +01:00
commit 1747a8788e
10 changed files with 86 additions and 12 deletions

View file

@ -1230,6 +1230,7 @@ internal sealed class AuthoritySmokeChecks
Phase = "pre-wave"
}).ConfigureAwait(false);
await node.FlushAsync().ConfigureAwait(false);
await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
var catalog = await node.RuntimeCatalog().LatestAsync().ConfigureAwait(false);
Require(catalog.Corporations.Any(corporation => !string.IsNullOrWhiteSpace(corporation.CorporationKey)),

View file

@ -5860,6 +5860,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
AetheriaDaemonNativeCatalog.EnsureAsync(node).GetAwaiter().GetResult();
node.FlushAsync().GetAwaiter().GetResult();
node.RefreshRuntimeCatalogAsync().GetAwaiter().GetResult();
var catalog = node.RuntimeCatalog().Latest();
var hull = catalog.FindItem(AetheriaDaemonNativeCatalog.DockyardHullItemKey);
var bay = catalog.FindItem(AetheriaDaemonNativeCatalog.DockyardBayItemKey);

View file

@ -84,6 +84,8 @@ await AetheriaDaemonNativeCatalog.EnsureAsync(node).ConfigureAwait(false);
TraceStartup("native-catalog");
await node.FlushAsync().ConfigureAwait(false);
TraceStartup("initial-flush");
await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
TraceStartup("runtime-catalog");
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
TraceStartup("playable-run");
await EnsureGameSessionAsync(node, options, startedAtUtc).ConfigureAwait(false);
@ -1991,7 +1993,7 @@ static async Task PublishDaemonMenuSurfacesAsync(
var updatedAtUtc = string.IsNullOrWhiteSpace(frame.PublishedAtUtc)
? DateTimeOffset.UtcNow.ToString("O")
: frame.PublishedAtUtc;
var catalog = AetheriaRuntimeCatalogStore.OpenReadOnly(node.StatePath);
var catalog = node.RuntimeCatalog().Latest();
var assetManifest = AetheriaRuntimeAssets.ProjectManifest(
catalog,
frame.Run?.RunId ?? frame.SessionId,

View file

@ -210,6 +210,7 @@ if (args.Any(argument => string.Equals(argument, "--merge-mine", StringCompariso
await mergeNode.MutableDocument<AetheriaItemDefinition>(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(mine.LegacyId))
.ReplaceAsync(mine);
await mergeNode.FlushAsync();
await mergeNode.RefreshRuntimeCatalogAsync();
var merged = await mergeNode.RuntimeCatalog().LatestAsync().ConfigureAwait(false);
var mergedMine = merged.FindItem(mine.ItemKey);
if (mergedMine == null || !mergedMine.BehaviorPayloads.Any(behavior =>
@ -229,6 +230,7 @@ if (args.Any(argument => string.Equals(argument, "--merge-station-sensor", Strin
await mergeNode.MutableDocument<AetheriaItemDefinition>(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(stationHull.LegacyId))
.ReplaceAsync(stationHull);
await mergeNode.FlushAsync();
await mergeNode.RefreshRuntimeCatalogAsync();
var merged = await mergeNode.RuntimeCatalog().LatestAsync().ConfigureAwait(false);
var mergedHull = merged.FindItem(stationHull.ItemKey);
if (mergedHull == null || !(mergedHull.Hardpoints ?? []).Any(hardpoint =>
@ -338,7 +340,7 @@ foreach (var nameFile in parsedNameFiles)
}
await node.FlushAsync();
var runtimeCatalog = await node.RuntimeCatalog().LatestAsync().ConfigureAwait(false);
var runtimeCatalog = await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
var importedCorporationKeys = runtimeCatalog.Corporations
.Select(corporation => corporation.CorporationKey)
.Where(key => !string.IsNullOrWhiteSpace(key))
@ -369,6 +371,7 @@ await node.MutableDocument<AetheriaTradeValuePolicy>(AetheriaStateNode.TradeValu
runtimeCatalog.TradeValueSettings,
capturedAtUtc));
await node.FlushAsync();
await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
await node.CatalogSurface().LatestAsync().ConfigureAwait(false);
await node.FlushAsync();

View file

@ -98,6 +98,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
});
await node.FlushAsync();
await node.RefreshRuntimeCatalogAsync();
var runtimeCatalog = await node.RuntimeCatalog().LatestAsync().ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaStateNode.CatalogSurfaceKey)
.ReplaceAsync(AetheriaEveSurfaceDocuments.BuildCatalogSurface(runtimeCatalog, now));

View file

@ -126,6 +126,7 @@ await using var node = await AetheriaStateNode.OpenAsync(
verificationStatePath,
"aetheria-state-verify",
enableDurableShardLogs: false);
await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
var ledger = await node.MutableDocument<AetheriaMigrationLedger>(AetheriaStateNode.MigrationLedgerKey).ReadAsync()
?? throw new InvalidOperationException("Missing typed migration ledger.");
@ -12985,6 +12986,7 @@ static void RequireUnityRuntimeCatalogClientUsesManagedDocument(string root)
static void RequireCatalogSurfaceUsesManagedRuntimeCatalog(string root)
{
var stateNodePath = Path.Combine(root, "Aetheria.State", "AetheriaStateNode.cs");
var surfaceDocumentsPath = Path.Combine(root, "Aetheria.State", "AetheriaEveSurfaceDocuments.cs");
var bridgePath = Path.Combine(root, "Aetheria.State", "AetheriaEveCommandBridge.cs");
var importPath = Path.Combine(root, "Aetheria.State.Import", "Program.cs");
@ -12998,6 +13000,37 @@ static void RequireCatalogSurfaceUsesManagedRuntimeCatalog(string root)
var import = File.Exists(importPath)
? File.ReadAllText(importPath)
: throw new InvalidOperationException("Cannot verify legacy catalog import; Aetheria.State.Import/Program.cs is missing.");
var stateNode = File.Exists(stateNodePath)
? File.ReadAllText(stateNodePath)
: throw new InvalidOperationException("Cannot verify runtime catalog ownership; AetheriaStateNode.cs is missing.");
var requiredStateNodeSymbols = new[]
{
"Database.GetAsync<AetheriaRuntimeCatalogSnapshot>(RuntimeCatalogKey)",
"Database.WatchRecord<AetheriaRuntimeCatalogSnapshot>(RuntimeCatalogKey)",
"public async Task<AetheriaRuntimeCatalogSnapshot> RefreshRuntimeCatalogAsync()",
"await Database.PutAsync(RuntimeCatalogKey, snapshot).ConfigureAwait(false);",
"_ => catalog.Watch()"
};
var missingStateNodeSymbols = requiredStateNodeSymbols
.Where(symbol => !stateNode.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingStateNodeSymbols.Length > 0)
{
throw new InvalidOperationException(
"The state node must own one explicitly refreshed runtime catalog document: " +
string.Join(", ", missingStateNodeSymbols));
}
var runtimeCatalogMethod = stateNode[
stateNode.IndexOf("public CultMeshDocumentHandle<AetheriaRuntimeCatalogSnapshot> RuntimeCatalog()", StringComparison.Ordinal)..
stateNode.IndexOf("public CultMeshDocumentHandle<EveSurfaceDocument> CatalogSurface()", StringComparison.Ordinal)];
if (runtimeCatalogMethod.Contains("DaemonFrameLatest", StringComparison.Ordinal) ||
runtimeCatalogMethod.Contains("OpenReadOnly(StatePath)", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Daemon frames must not invalidate or recompile the runtime catalog.");
}
var requiredSurfaceDocumentSymbols = new[]
{
@ -13068,6 +13101,7 @@ static void RequireCatalogSurfaceUsesManagedRuntimeCatalog(string root)
"bb5d9f73-612f-4060-8fb4-f72d6b93b616",
"rarity:rare",
"--merge-station-sensor",
"RefreshRuntimeCatalogAsync()",
"await node.CatalogSurface().LatestAsync().ConfigureAwait(false);"
};
var missingImportSymbols = requiredImportSymbols

View file

@ -32,6 +32,7 @@ public static class AetheriaDocumentRegistry
typeof(AetheriaMigrationLedger),
typeof(AetheriaLegacyCatalogQuarantine),
typeof(AetheriaTradeValuePolicy),
typeof(AetheriaRuntimeCatalogSnapshot),
typeof(AetheriaPlayerSettings),
typeof(AetheriaLoadoutTemplate),
typeof(AetheriaRunState),

View file

@ -54,7 +54,8 @@ public static class AetheriaEveCommandBridge
switch (command.Kind)
{
case AetheriaRuntimeEveCommandKind.CatalogRefresh:
var catalog = await node.RuntimeCatalog().LatestAsync().ConfigureAwait(false);
await node.FlushAsync().ConfigureAwait(false);
var catalog = await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
await node.MutableDocument<EveSurfaceDocument>(AetheriaStateNode.CatalogSurfaceKey)
.ReplaceAsync(AetheriaEveSurfaceDocuments.BuildCatalogSurface(catalog, command.IssuedAtUtc))
.ConfigureAwait(false);

View file

@ -109,11 +109,13 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
return _runtimeCatalog ??= CultMesh.Document(
"aetheria.catalog.runtime",
CultMesh.Verse("aetheria.local", RuntimeId),
_ => Task.FromResult(AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath)),
_ => Database.WatchRecord<AetheriaRuntimeDaemonFrameDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
async _ => await Database.GetAsync<AetheriaRuntimeCatalogSnapshot>(RuntimeCatalogKey)
.ConfigureAwait(false)
?? throw new InvalidDataException(
"The compiled Aetheria runtime catalog is missing. Refresh it after changing source catalog records."),
_ => Database.WatchRecord<AetheriaRuntimeCatalogSnapshot>(RuntimeCatalogKey)
.Where(change => change.Document != null)
.Select(_ => AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath)),
.Select(change => change.Document!),
sources: new[]
{
CultMesh.ProjectionSource("catalog:aetheria.runtime"),
@ -134,11 +136,9 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
_ => Task.FromResult(AetheriaEveSurfaceDocuments.BuildCatalogSurface(
catalog.Latest(),
DateTimeOffset.UtcNow.ToString("O"))),
_ => Database.WatchRecord<AetheriaRuntimeDaemonFrameDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
.Where(change => change.Document != null)
.Select(_ => AetheriaEveSurfaceDocuments.BuildCatalogSurface(
catalog.Latest(),
_ => catalog.Watch()
.Select(snapshot => AetheriaEveSurfaceDocuments.BuildCatalogSurface(
snapshot,
DateTimeOffset.UtcNow.ToString("O"))),
sources: new[]
{
@ -154,6 +154,13 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria typed catalog Eve surface"));
}
public async Task<AetheriaRuntimeCatalogSnapshot> RefreshRuntimeCatalogAsync()
{
var snapshot = AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath);
await Database.PutAsync(RuntimeCatalogKey, snapshot).ConfigureAwait(false);
return snapshot;
}
public Task<CultRecordHandle<AetheriaRuntimeDaemonCommandDocument>> SubmitDaemonCommandAsync(
AetheriaRuntimeDaemonCommandDocument command)
{
@ -242,6 +249,9 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
public static CultRecordKey CatalogSurfaceKey { get; } =
new(AetheriaEveSurfaceDocuments.CatalogSurfaceKey);
public static CultRecordKey RuntimeCatalogKey { get; } =
new("global:aetheria.runtime_catalog.v1");
public static CultRecordKey OperationsSurfaceKey { get; } =
new(AetheriaEveSurfaceDocuments.OperationsSurfaceKey);

View file

@ -3607,6 +3607,26 @@ Live client readiness and durable publication are separate authorities:
the released generic EveUnity witness proves provider discovery, mapped
assets, QUIC SoA, commands and receipts while persistence runs concurrently.
The runtime catalog has one compiled-state owner:
- Owner: `AetheriaStateNode` owns the compiled
`AetheriaRuntimeCatalogSnapshot`; item, corporation, name-file, and trade
policy documents remain the authored source records.
- Inputs: an explicit catalog refresh reads the durable source records after
their write has been flushed.
- Outputs: one typed runtime-catalog record feeds simulation, asset projection,
loadout generation, and the Eve catalog surface.
- Derived state: the Eve catalog surface is a projection of that record. Daemon
frames and their timestamps have no catalog-freshness authority.
- Forbidden writers: simulation ticks, daemon-frame publication, Unity clients,
and surface refreshes cannot silently recompile or replace the catalog.
- Shared paths: import, daemon-native catalog augmentation, and the explicit Eve
catalog-refresh command use the same compile-and-publish primitive.
- Cut line: runtime reads and catalog-surface watches no longer reopen the
CultCache store or subscribe to daemon-frame changes.
- Verification layer: state verification requires the typed record/watch path
and rejects daemon-frame invalidation or disk parsing inside `RuntimeCatalog`.
Remote realtime transport has one narrow owner:
- Owner: the Aetheria client CultMesh host owns the provider QUIC listener;