Make runtime catalog a typed cached authority
This commit is contained in:
parent
46bfd7d241
commit
1747a8788e
10 changed files with 86 additions and 12 deletions
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public static class AetheriaDocumentRegistry
|
|||
typeof(AetheriaMigrationLedger),
|
||||
typeof(AetheriaLegacyCatalogQuarantine),
|
||||
typeof(AetheriaTradeValuePolicy),
|
||||
typeof(AetheriaRuntimeCatalogSnapshot),
|
||||
typeof(AetheriaPlayerSettings),
|
||||
typeof(AetheriaLoadoutTemplate),
|
||||
typeof(AetheriaRunState),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue