diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 54ef3c3f4..0473a60bc 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index 1f34b5abb..247d3908a 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -129,8 +129,16 @@ catalog reader. It also proves runtime commit envelopes for settings and run zone/entity snapshots can be queued, applied through `Aetheria.State`, and cleared. -`Aetheria.State.ApplyPending` is the bounded local applicator for queued Unity -runtime commits: +`Economy.Server` opens the CultMesh state host, applies pending runtime commits +on startup, and polls for new pending commits while running. For a bounded +one-shot drain without leaving the host running: + +```powershell +dotnet run --project .\Economy.Server\Economy.Server.csproj -- --apply-pending-once +``` + +`Aetheria.State.ApplyPending` is the smaller local operator applicator for +queued Unity runtime commits when the server host is not being used: ```powershell dotnet run --project .\Aetheria.State.ApplyPending\Aetheria.State.ApplyPending.csproj -- . diff --git a/Economy.Server/Program.cs b/Economy.Server/Program.cs index e653b7e46..bacd10b21 100644 --- a/Economy.Server/Program.cs +++ b/Economy.Server/Program.cs @@ -16,10 +16,17 @@ internal static class Program startServer: true).ConfigureAwait(false); await EnsureWorldDocumentAsync(node).ConfigureAwait(false); + await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false); await node.FlushAsync().ConfigureAwait(false); + if (HasFlag(args, "--apply-pending-once")) + { + Console.WriteLine("Aetheria pending runtime commit drain completed."); + return; + } + Console.WriteLine("Aetheria CultMesh state host is running. Press Ctrl+C to stop."); - await WaitForShutdownAsync().ConfigureAwait(false); + await RunUntilShutdownAsync(node, PendingInterval(args)).ConfigureAwait(false); Console.WriteLine("Aetheria CultMesh state host stopping."); } @@ -59,7 +66,24 @@ internal static class Program }).ConfigureAwait(false); } - private static Task WaitForShutdownAsync() + private static async Task ApplyPendingRuntimeCommitsAsync(AetheriaStateNode node) + { + var report = await AetheriaRuntimeCommitLogApplier.ApplyPendingAsync(node).ConfigureAwait(false); + if (report.AppliedPaths.Length == 0) + { + Console.WriteLine("No pending Aetheria runtime commits."); + return; + } + + Console.WriteLine( + "Applied pending Aetheria runtime commits: " + + $"settings={report.AppliedPlayerSettings}, " + + $"loadouts={report.AppliedLoadoutTemplates}, " + + $"runs={report.AppliedRunCheckpoints}, " + + $"files={report.AppliedPaths.Length}"); + } + + private static async Task RunUntilShutdownAsync(AetheriaStateNode node, TimeSpan pendingInterval) { var stopped = new TaskCompletionSource(); Console.CancelKeyPress += (_, eventArgs) => @@ -68,6 +92,34 @@ internal static class Program stopped.TrySetResult(null); }; - return stopped.Task; + while (!stopped.Task.IsCompleted) + { + var delay = Task.Delay(pendingInterval); + var completed = await Task.WhenAny(stopped.Task, delay).ConfigureAwait(false); + if (completed == stopped.Task) + break; + + await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false); + } + } + + private static TimeSpan PendingInterval(IReadOnlyList args) + { + for (var i = 0; i < args.Count - 1; i++) + { + if (string.Equals(args[i], "--pending-interval-ms", StringComparison.OrdinalIgnoreCase) && + int.TryParse(args[i + 1], out var milliseconds) && + milliseconds > 0) + { + return TimeSpan.FromMilliseconds(milliseconds); + } + } + + return TimeSpan.FromSeconds(5); + } + + private static bool HasFlag(IReadOnlyList args, string flag) + { + return args.Any(arg => string.Equals(arg, flag, StringComparison.OrdinalIgnoreCase)); } } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index b8fb123f0..9f467826d 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -41,9 +41,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. files, and generated keyboard layout caches no longer write bespoke durable files. The legacy `PlayerSettings` runtime object is no longer decorated as a MessagePack - persistence shape. `Aetheria.State.ApplyPending` is the repo-local applicator - entrypoint for draining `aetheria-world.cc.pending` into canonical typed state - until that worker is hosted as a daemon/CultMesh organ. + persistence shape. `Economy.Server` now drains `aetheria-world.cc.pending` + into canonical typed state on startup and on a daemon polling loop while + hosting the CultMesh state node. `Aetheria.State.ApplyPending` remains a + bounded local operator applicator. - Shared domain state is still partly built around `DatabaseEntry`, GUID identity, MessagePack attributes, and static cache references. Newtonsoft, JsonKnownTypes, RethinkDB, LiteNetLib client transport, and the broken @@ -117,7 +118,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `ActionGameManager` binds the typed runtime item catalog projected from `aetheria-world.cc`. MessagePack catalog construction no longer grabs global `DatabaseLinkBase` authority. -- `Economy.Server` now starts the modern `Aetheria.State` CultMesh node and no +- `Economy.Server` now starts the modern `Aetheria.State` CultMesh node, drains + pending Unity runtime commits through the typed state applicator, and no longer owns RethinkDB/LiteNetLib state. - `Aetheria.State.Import` writes a typed quarantine manifest and migration ledger for the legacy catalog files. It also raw-decodes stable old @@ -170,6 +172,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. successfully applied command files by default. It is an operational bridge, not a second state owner; the applicator delegates all writes to `AetheriaStateNode`. +- `Economy.Server` hosts the CultMesh state node and now owns the long-running + pending runtime commit drain loop. `--apply-pending-once` runs the same drain + path once for smoke/operator use without keeping the process alive. - `AetheriaCatalogSurfaceProjector` now emits the first provider-owned Eve surface from typed catalog state. The importer materializes a `gamecult.eve.surface.v1` catalog operator document at @@ -530,6 +535,8 @@ First Aetheria surfaces to publish: documents. - Done: add `Aetheria.State.ApplyPending` as a bounded local applicator for queued runtime commits until the drain loop is hosted as a daemon. + - Done: move the pending runtime commit drain into `Economy.Server` startup + and daemon polling, with `--apply-pending-once` for bounded operation. - Done: stop legacy catalog pull/read paths from writing entries back to their source backing store. - Done: delete legacy catalog backing-store write/realtime APIs.